Agent开发实战:让AI自己决定调哪个工具,踩了4个坑才知道“自主决策“没那么简单
Java后端写业务逻辑,调接口是service.callApi(),流程是人定的。Agent不一样——LLM自己决定调哪个工具、传什么参数、看返回结果再决定下一步。听起来美好,实际踩了4个坑差点放弃。
坑1:工具描述模糊,LLM死活不调工具——只会自己瞎猜答案
翻车现场:
from langchain_core.tools import tool
# 工具描述写得太笼统
@tool
def search_database(query: str) -> str:
"""搜索数据库""" # ❌ 这描述等于没写
import sqlite3
conn = sqlite3.connect("sales.db")
result = conn.execute(query).fetchall()
conn.close()
return str(result)
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""发邮件""" # ❌ 什么时候该发邮件?什么内容?LLM不知道
import smtplib
# ... 发邮件逻辑
return f"邮件已发送给{to}"
# 创建Agent
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
llm = ChatOpenAI(model="gpt-4o-mini")
tools = [search_database, send_email]
agent = create_tool_calling_agent(llm, tools)
executor = AgentExecutor(agent=agent, tools=tools)
# 用户问:上个月华东区销售额是多少?
result = executor.invoke({"input": "上个月华东区销售额是多少?"})
# ❌ LLM回答:"根据我的知识,华东区销售额大约在500万左右"
# ❌ 完全没调search_database工具!LLM觉得自己"知道"答案,直接瞎编
原因: 工具描述"搜索数据库"太模糊——LLM不知道这个工具能查什么、什么时候该用它。就像Java里接口文档只写了"getData()"没写参数含义和返回格式,调用方根本不知道什么时候该调。
修复方案——工具描述5要素模板:
from langchain_core.tools import tool
from pydantic import BaseModel, Field
# ============ 正确的工具定义方式 ============
class DatabaseQueryInput(BaseModel):
"""数据库查询参数——像Java方法的@RequestBody"""
query: str = Field(
description="SQL查询语句,只支持SELECT。示例:SELECT region, SUM(amount) FROM sales WHERE month='2026-05'"
)
region: str = Field(
description="查询区域,可选:华东/华南/华北/西部。与query配合使用可缩小范围"
)
@tool(args_schema=DatabaseQueryInput)
def search_database(query: str, region: str = "") -> str:
"""查询公司销售数据库,支持按区域、时间、产品等维度查询销售额、订单数、客户数等数据。
何时使用:用户问到具体的销售数据、订单数量、业绩指标时使用此工具。
不能做什么:不支持INSERT/UPDATE/DELETE,不支持查询非销售数据。"""
import sqlite3
conn = sqlite3.connect("sales.db")
actual_query = query
if region:
actual_query += f" AND region='{region}'"
result = conn.execute(actual_query).fetchall()
conn.close()
return str(result) if result else "未查询到数据"
class EmailInput(BaseModel):
"""邮件参数"""
to: str = Field(description="收件人邮箱地址")
subject: str = Field(description="邮件主题,简洁明确")
body: str = Field(description="邮件正文内容")
@tool(args_schema=EmailInput)
def send_email(to: str, subject: str, body: str) -> str:
"""发送工作邮件。何时使用:用户明确要求发送邮件、通知某人、或需要将结果转发给同事时。
不能做什么:不能发垃圾邮件、不能发匿名邮件。"""
import smtplib
# ... 发邮件逻辑
return f"邮件已发送给{to}"
tools = [search_database, send_email]
agent = create_tool_calling_agent(llm, tools)
executor = AgentExecutor(agent=agent, tools=tools)
# ✅ 现在LLM会自动调工具
result = executor.invoke({"input": "上个月华东区销售额是多少?"})
# ✅ LLM判断:用户问具体销售数据→调search_database→SQL="SELECT SUM(amount) FROM sales WHERE month='2026-05' AND region='华东'"
# ✅ 回答:"上个月华东区销售额为856万元"
Java类比: 工具描述≈Swagger的@ApiModel+@ApiModelProperty——没有文档的接口就是废接口。Java里你不会调一个没有文档的微服务,同理LLM不会调一个没有描述的工具。
| 描述质量 | LLM行为 | 结果准确率 | Java类比 |
|---|---|---|---|
| 无描述/模糊描述 | 不调工具,自己猜 | 0%-30% | 没文档的接口 |
| 有基本描述 | 有时调有时不调 | 50%-70% | 只有方法名的接口 |
| 5要素完整描述 | 准确判断+正确调用 | 90%+ | 完整Swagger文档 |
工具描述5要素:
- 做什么:功能定义("查询销售数据库")
- 何时用:触发条件("用户问到具体销售数据时")
- 不能做:边界限制("不支持INSERT/DELETE")
- 参数说明:每个参数的含义和示例
- 返回格式:输出是什么类型/结构
坑2:Agent死循环——调了工具看到结果不满意,反复调同一个工具
翻车现场:
# 用户问:帮我查一下今天的股价然后发邮件给老板
executor = AgentExecutor(
agent=agent,
tools=[search_database, send_email, get_stock_price],
max_iterations=15 # 默认允许15轮循环
)
result = executor.invoke({"input": "查今天腾讯股价,发给boss@company.com"})
# ❌ Agent执行过程:
# 第1轮:调get_stock_price→返回"腾讯股价380元"
# 第2轮:觉得不确定,再调get_stock_price→返回"腾讯股价380元"(一样)
# 第3轮:还是不确定,又调get_stock_price→返回"腾讯股价380元"
# ... 重复15轮,最后timeout报错
# ❌ Agent永远不进入"发邮件"步骤
原因: LLM没有"确认机制"——看到工具返回结果后,不知道什么时候该"满意"并进入下一步。就像Java里没有循环退出条件的while(true),必然死循环。
修复方案——3层防护:
# ============ 第1层:限制最大迭代次数 ============
executor = AgentExecutor(
agent=agent,
tools=[search_database, send_email, get_stock_price],
max_iterations=5, # ✅ 从15改为5,强制最多5轮
early_stopping_method="generate" # ✅ 超过5轮后直接生成最终回答而不是报错
)
# ============ 第2层:Prompt中加入"确认规则" ============
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", """你是一个销售数据分析助手。工作规则:
1. 每个工具最多调用1次,不要重复调用同一个工具
2. 工具返回结果后,直接基于结果回答或执行下一步,不要反复验证
3. 拿到数据后立即执行用户要求的后续动作(如发邮件)
4. 如果工具返回"未查询到数据",尝试调整查询条件1次,如果还是没有就如实告知用户"""),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
# ============ 第3层:自定义工具加入缓存 ============
from functools import lru_cache
@lru_cache(maxsize=32) # ✅ 同样参数的工具调用只执行1次
@tool(args_schema=StockInput)
def get_stock_price(symbol: str) -> str:
"""查询股票实时价格。何时使用:用户问到股价时。"""
# ... 查询逻辑
return f"{symbol}当前股价:380元"
# ✅ 现在Agent执行过程:
# 第1轮:调get_stock_price→返回"腾讯股价380元"(缓存)
# 第2轮:直接调send_email→"邮件已发送给boss@company.com"
# ✅ 2轮搞定,不再死循环
Java类比: 死循环≈while(true)没有退出条件;max_iterations≈for(int i=0; i<5; i++)限制循环次数;缓存≈@Cacheable——同样参数的查询只执行一次;Prompt规则≈业务规则校验——if(result != null) { nextStep(); }明确下一步。
| 防护层 | 作用 | Java类比 |
|---|---|---|
| max_iterations=5 | 硬性兜底,最多5轮 | for循环上限 |
| Prompt确认规则 | 软性引导,告诉LLM何时停止 | 业务规则校验 |
| @lru_cache | 工具级防重,同参数只调1次 | @Cacheable |
坑3:Agent不会规划多步任务——一步到位思维导致漏步骤
翻车现场:
# 用户问一个需要多步的任务
result = executor.invoke({
"input": "分析华东区上个月销售数据,找出下降最多的产品,然后发邮件给区域经理建议调整策略"
})
# ❌ Agent直接调send_email,邮件内容写"华东区销售下降,建议调整策略"
# ❌ 没有先调search_database查数据
# ❌ 没有分析哪个产品下降最多
# ❌ 邮件内容全是空话,没有数据支撑
原因: create_tool_calling_agent是单步决策——每轮只决定"下一步调什么工具",不会先做整体规划。就像Java里没有业务流程编排的微服务,每个服务只知道自己的事,不知道整体流程。
修复方案——ReAct+Prompt引导多步规划:
# ============ 方案1:Prompt中加入ReAct思维框架 ============
react_prompt = ChatPromptTemplate.from_messages([
("system", """你是一个销售数据分析助手,使用ReAct框架工作。
每次收到任务,你必须先思考再行动:
1. Thought:分析用户需求,列出需要完成的步骤
2. Action:选择最合适的工具执行当前步骤
3. Observation:观察工具返回结果
4. Thought:基于观察结果,判断下一步
5. 重复2-4直到所有步骤完成
6. Final Answer:汇总所有结果给出最终回答
关键原则:
- 收到多步任务时,先在Thought中列出完整步骤计划
- 按步骤顺序执行,不要跳步
- 每步完成后才能进入下一步"""),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(llm, tools, react_prompt)
executor = AgentExecutor(agent=agent, tools=tools, max_iterations=10)
# ✅ 现在Agent会规划多步:
# Thought: 用户要求3步任务:①查数据 ②分析下降最多的产品 ③发邮件
# Action: search_database("SELECT product, amount FROM sales WHERE region='华东' AND month='2026-05'")
# Observation: [("手机", 120万), ("电脑", 85万), ("平板", 45万)]
# Thought: 上月数据拿到了,需要对比上月才知道谁下降最多
# Action: search_database("SELECT product, amount FROM sales WHERE region='华东' AND month='2026-04'")
# Observation: [("手机", 150万), ("电脑", 90万), ("平板", 60万)]
# Thought: 手机下降30万(20%)最多
# Action: send_email(to="manager@company.com", subject="华东区销售下降分析", body="手机品类下降20%...")
# ✅ 4步完成,有数据有分析有邮件
# ============ 方案2:更复杂的任务用LangGraph做流程编排 ============
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
class AgentState(TypedDict):
"""Agent状态——像Java的ProcessContext"""
task: str
plan: list[str]
current_step: int
observations: Annotated[list[str], operator.add]
final_answer: str
def plan_task(state: AgentState) -> AgentState:
"""步骤1:规划任务分解"""
task = state["task"]
plan_prompt = f"""将以下任务分解为具体的执行步骤列表:
任务:{task}
每步必须明确:调什么工具、传什么参数。
输出格式:JSON数组,每个元素是{"step": "步骤描述", "tool": "工具名", "args": "参数"}"""
plan_result = llm.invoke(plan_prompt)
# 解析出步骤列表
steps = eval(plan_result.content) # 实际应用中用json.loads+Pydantic
return {"plan": steps, "current_step": 0, "observations": []}
def execute_step(state: AgentState) -> AgentState:
"""步骤2:按计划执行每一步"""
step_idx = state["current_step"]
if step_idx >= len(state["plan"]):
return {"current_step": step_idx}
step = state["plan"][step_idx]
result = executor.invoke({"input": step["step"]})
return {
"observations": [result["output"]],
"current_step": step_idx + 1
}
def should_continue(state: AgentState) -> str:
"""步骤3:判断是否还有下一步"""
if state["current_step"] >= len(state["plan"]):
return "summarize"
return "execute"
def summarize(state: AgentState) -> AgentState:
"""步骤4:汇总所有步骤结果"""
all_observations = "\n".join(state["observations"])
summary_prompt = f"""基于以下所有步骤的执行结果,给出最终回答:
{all_observations}"""
final_answer = llm.invoke(summary_prompt).content
return {"final_answer": final_answer}
# 构建LangGraph工作流
graph = StateGraph(AgentState)
graph.add_node("plan", plan_task)
graph.add_node("execute", execute_step)
graph.add_node("summarize", summarize)
graph.add_edge("plan", "execute")
graph.add_conditional_edges("execute", should_continue, {
"execute": "execute",
"summarize": "summarize"
})
graph.add_edge("summarize", END)
graph.set_entry_point("plan")
app = graph.compile()
# ✅ 执行多步任务
result = app.invoke({"task": "分析华东区销售数据,找下降最多的产品,发邮件给经理"})
# ✅ plan→execute(查数据)→execute(查对比)→execute(发邮件)→summarize(汇总)
Java类比:
create_tool_calling_agent≈单次controller.callService()——只知道下一步调谁- ReAct Prompt≈业务规则注释——引导执行顺序但不是硬约束
- LangGraph≈Spring BPMN流程引擎——先规划后执行,流程图可视化,每步有明确节点
| 方案 | 流程保证 | 适用场景 | Java类比 |
|---|---|---|---|
| 单步Agent | ❌无保证 | 单工具任务 | 单次接口调用 |
| ReAct+Prompt | ⚠️软引导 | 2-3步任务 | 业务规则注释 |
| LangGraph | ✅硬保证 | 多步复杂任务 | BPMN流程引擎 |
坑4:Agent没有记忆——每轮对话都从零开始,重复查数据重复发邮件
翻车现场:
# 第1轮对话
result1 = executor.invoke({"input": "查华东区上月销售额"})
# ✅ Agent调search_database→返回"856万"
# 第2轮对话(同一个executor)
result2 = executor.invoke({"input": "把刚才查到的销售额发邮件给经理"})
# ❌ Agent又调了一次search_database!不知道"刚才"查过什么
# ❌ 因为Agent没有记忆,每轮对话独立,之前的查询结果全丢了
原因: AgentExecutor默认不保留历史对话——每轮invoke是独立的。就像Java的REST API每次请求都是新session,没有HttpSession就没有状态。
修复方案——3种记忆方案实测对比:
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# ============ 方案1:滑动窗口记忆(最简单) ============
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(
k=5, # ✅ 只保留最近5轮对话,防止token溢出
return_messages=True
)
executor_with_memory = AgentExecutor(
agent=agent,
tools=tools,
memory=memory, # ✅ Agent现在记住最近5轮了
max_iterations=5
)
# ✅ 第1轮:查销售额→记住"856万"
# ✅ 第2轮:"刚才查到的"→Agent从记忆里找到856万→直接调send_email
# ============ 方案2:摘要记忆(长对话不溢出) ============
from langchain.memory import ConversationSummaryMemory
summary_memory = ConversationSummaryMemory(
llm=llm, # ✅ 用LLM自动总结历史,不存原文
return_messages=True
)
# 对话到第10轮时,memory内容从10条原文变成1条摘要:
# "用户之前查询了华东区销售数据(856万),华南区(620万),要求分析下降原因并发邮件给经理"
# ✅ 10条→1条摘要,token占用大幅降低
# ============ 方案3:向量长期记忆(跨任务检索) ============
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
# 长期记忆存到向量库
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectorstore = Chroma(embedding_function=embeddings, persist_directory="./agent_memory")
# 存储:每次对话结束,把重要信息存入向量库
def save_to_long_term_memory(query, result):
"""像Java的数据库持久化——对话结束不丢数据"""
doc = f"用户问:{query},结果:{result}"
vectorstore.add_texts([doc])
# 检索:下次对话开始前,从向量库找相关信息
def retrieve_from_memory(query, top_k=3):
"""像Java的缓存预热——启动前先加载相关数据"""
results = vectorstore.similarity_search(query, k=top_k)
return "\n".join([r.page_content for r in results])
# 完整记忆Agent
memory = ConversationBufferWindowMemory(k=5, return_messages=True)
executor_with_memory = AgentExecutor(
agent=agent, tools=tools, memory=memory, max_iterations=5
)
def smart_agent_invoke(user_input):
"""带长期记忆的Agent调用"""
# 先从长期记忆检索相关信息
relevant_history = retrieve_from_memory(user_input)
# 把检索到的历史信息拼到Prompt里
enhanced_input = f"{user_input}\n\n相关历史信息:{relevant_history}"
result = executor_with_memory.invoke({"input": enhanced_input})
# 保存到长期记忆
save_to_long_term_memory(user_input, result["output"])
return result
3种记忆实测对比:
| 记忆类型 | 保留轮数 | Token占用 | 跨任务 | 适用场景 | Java类比 |
|---|---|---|---|---|---|
| 滑动窗口(k=5) | 最近5轮 | ~2000 | ❌不跨 | 简单多轮对话 | HttpSession |
| 摘要记忆 | 全部(压缩) | ~500 | ❌不跨 | 长对话 | Session+压缩 |
| 窗口+向量长期 | 5轮+全部检索 | 2000+检索 | ✅跨任务 | 生产级Agent | Session+Redis |
Java类比: 滑动窗口≈HttpSession(只存当前会话);摘要≈Session压缩(大对象→摘要DTO);长期向量记忆≈Redis缓存+MySQL持久化——会话结束后数据不丢,下次启动还能用。
完整实战代码:带记忆+规划+多工具的ReAct Agent
把4个坑的修复全部串起来:
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain.memory import ConversationBufferWindowMemory
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from functools import lru_cache
# ============ 1. 工具定义(坑1修复:5要素描述+Pydantic Schema) ============
class SalesQueryInput(BaseModel):
query: str = Field(description="SQL查询,只支持SELECT。示例:SELECT region, SUM(amount) FROM sales WHERE month='2026-05'")
region: str = Field(description="区域:华东/华南/华北/西部")
@tool(args_schema=SalesQueryInput)
@lru_cache(maxsize=32) # 坑2修复:缓存防重
def search_sales(query: str, region: str = "") -> str:
"""查询公司销售数据库,支持按区域、时间、产品查询销售额、订单数等。
何时使用:用户问到具体销售数据、业绩指标时。
限制:不支持INSERT/UPDATE/DELETE。"""
import sqlite3
conn = sqlite3.connect("sales.db")
actual_query = query + (f" AND region='{region}'" if region else "")
result = conn.execute(actual_query).fetchall()
conn.close()
return str(result) if result else "未查询到数据"
class EmailInput(BaseModel):
to: str = Field(description="收件人邮箱")
subject: str = Field(description="邮件主题")
body: str = Field(description="邮件正文")
@tool(args_schema=EmailInput)
def send_email(to: str, subject: str, body: str) -> str:
"""发送工作邮件。何时使用:用户明确要求发邮件或转发结果时。
限制:不能发垃圾邮件。"""
# 发邮件逻辑...
return f"邮件已发送给{to},主题:{subject}"
# ============ 2. Prompt(坑3修复:ReAct思维+防循环规则) ============
react_prompt = ChatPromptTemplate.from_messages([
("system", """你是销售数据分析助手,使用ReAct框架工作:
1. Thought:先分析需求,列出步骤计划
2. Action:选择工具执行当前步骤
3. Observation:观察结果,判断下一步
4. 重复直到所有步骤完成
关键规则:
- 每个工具最多调1次(不要重复调)
- 收到多步任务先列出步骤再按顺序执行
- 工具返回结果后立即基于结果做下一步"""),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
# ============ 3. Agent创建 ============
llm = ChatOpenAI(model="gpt-4o-mini")
tools = [search_sales, send_email]
agent = create_tool_calling_agent(llm, tools, react_prompt)
# ============ 4. 记忆+执行器(坑2/4修复) ============
memory = ConversationBufferWindowMemory(k=5, return_messages=True)
executor = AgentExecutor(
agent=agent,
tools=tools,
memory=memory, # 坑4修复:带记忆
max_iterations=5, # 坑2修复:限制5轮
early_stopping_method="generate"
)
# ============ 5. 使用 ============
# 单步任务
result = executor.invoke({"input": "华东区上月销售额是多少?"})
# ✅ 调search_sales→返回856万
# 多步任务
result = executor.invoke({"input": "分析华东区下降最多的产品,发邮件给经理建议调整"})
# ✅ Thought→列出步骤:①查上月 ②查上上月 ③对比 ④发邮件
# ✅ 按步骤执行,4轮完成
# 带记忆的后续对话
result = executor.invoke({"input": "把刚才的分析结果再发给总监"})
# ✅ 从记忆找到分析结果→直接调send_email→不重复查数据
3种推理框架速查
| 框架 | 思路 | 适用场景 | 实现方式 | Java类比 |
|---|---|---|---|---|
| ReAct | 思考→行动→观察循环 | 需要调工具的实时任务 | Prompt引导+AgentExecutor | Service调用+结果判断 |
| CoT | 分步推理不调工具 | 纯推理/分析任务 | Prompt加"请一步步思考" | 算法分步实现 |
| ToT | 多路径探索选最优 | 战略决策/多方案对比 | LangGraph多分支 | 策略模式+评分选优 |
选择原则:
- 需要调工具 → ReAct(最常用)
- 纯分析推理 → CoT(最简单)
- 多方案选最优 → ToT(最复杂)
3种Agent框架选择决策
| 框架 | 适合什么 | 不适合什么 | 入门难度 | Java类比 |
|---|---|---|---|---|
| LangChain Agent | 单Agent+2-5个工具 | 复杂多步流程 | 低 | Spring MVC单Controller |
| LangGraph | 多步流程+条件分支 | 简单单工具任务 | 中 | BPMN流程引擎 |
| CrewAI/AutoGen | 多Agent协作 | 单Agent够用的任务 | 高 | 微服务团队 |
Java转AI的选择路径:
- 先学LangChain Agent——单Agent+ReAct,对应Java的单Service
- 流程复杂后升级LangGraph——多步编排,对应Java的流程引擎
- 需要团队协作时用CrewAI——多Agent分工,对应Java的微服务架构
4坑速查表
| 坑 | 现象 | 根因 | 修复 | Java类比 |
|---|---|---|---|---|
| 1 工具不调用 | LLM自己瞎猜答案 | 工具描述模糊 | 5要素描述+Pydantic Schema | Swagger文档 |
| 2 死循环 | 重复调同一个工具 | 没有退出条件 | max_iterations+缓存+Prompt规则 | for上限+@Cacheable |
| 3 不规划 | 直接跳到发邮件 | 单步决策无规划 | ReAct+LangGraph | BPMN流程引擎 |
| 4 无记忆 | 每轮重复查数据 | 没有历史对话 | 滑动窗口+向量长期记忆 | Session+Redis |
Agent开发踩了什么坑?评论区聊聊——从Java的"人定流程"到AI的"自主决策",思维模式要换,但工程问题的解法其实和Java很像:文档/缓存/流程编排/持久化,一个都少不了。
更多推荐



所有评论(0)