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要素:

  1. 做什么:功能定义("查询销售数据库")
  2. 何时用:触发条件("用户问到具体销售数据时")
  3. 不能做:边界限制("不支持INSERT/DELETE")
  4. 参数说明:每个参数的含义和示例
  5. 返回格式:输出是什么类型/结构

坑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的选择路径:

  1. 先学LangChain Agent——单Agent+ReAct,对应Java的单Service
  2. 流程复杂后升级LangGraph——多步编排,对应Java的流程引擎
  3. 需要团队协作时用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很像:文档/缓存/流程编排/持久化,一个都少不了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐