LangChain 的 Agent 为什么反复调用同一个工具 / 先打印中间步骤看工具返回值

文章导读
Agent 反复调用同一个工具,通常不是模型本身的问题,而是它在某一轮里没拿到能推动下一步的信息:工具抛的错被吞掉了、返回值是空、返回内容和模型期待的结构对不上,或者参数一直校验失败被重试。要分清是哪一种,比较省事的做法是把 Agent 每一步的动作记录打开,按“动作名 → 参数 → 工具原始返回 → 模型下一句”这四段依次看,而不是只盯着最终答案猜。
📋 目录
  1. 打开 Agent 的中间步骤回显
  2. 在工具函数内部打印入参与返回值
  3. 核对工具名称、描述与参数结构
  4. 设置迭代上限并观察停止位置
  5. 把工具报错原文与模型下一句动作对齐看
A A

Agent 反复调用同一个工具,通常不是模型本身的问题,而是它在某一轮里没拿到能推动下一步的信息:工具抛的错被吞掉了、返回值是空、返回内容和模型期待的结构对不上,或者参数一直校验失败被重试。要分清是哪一种,比较省事的做法是把 Agent 每一步的动作记录打开,按“动作名 → 参数 → 工具原始返回 → 模型下一句”这四段依次看,而不是只盯着最终答案猜。

先开启中间步骤回显,确认每一步的动作名与参数;再在工具函数内部打印入参和返回值,区分入参为空、返回为空、返回格式不对;然后核对工具名称、描述与参数结构,排除校验失败导致的重复重试。配合迭代上限观察 Agent 停在哪一步,并把工具报错原文与模型下一句动作按时间顺序对齐,才能判断这是死循环还是正常的多步推理。

打开 Agent 的中间步骤回显

回显打开后,你能看到模型每一步分别决定了什么动作、带了什么参数、观察到的结果是什么。以常见的 AgentExecutor 骨架为例,具体参数名请以你当前使用的 LangChain 版本为准:

from langchain.agents import AgentExecutor

executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,                    # 逐轮打印思考、动作与观察值
    return_intermediate_steps=True,  # 把每步的动作与返回一起带回来
)

result = executor.invoke({'input': '帮我查一下订单 A123 的状态'})
for action, observation in result['intermediate_steps']:
    print(action.tool, action.tool_input, repr(observation)[:300])

verbose 打开后,控制台里通常按轮次出现动作名、动作入参、观察值这三类信息,字段名大多是 action、action_input、observation;用 return_intermediate_steps 拿到的列表里,每一项是一对(AgentAction, observation),其中 AgentAction 上的 tool 就是被调用的工具名,tool_input 是模型给出的参数。如果 verbose 输出滚动太快看不全,可以挂一个回调处理器,在 on_tool_start、on_tool_end、on_agent_action 这几个钩子里把事件写到文件,按时间回看更清楚。

在工具函数内部打印入参与返回值

回显只能证明“模型决定调用”,不能证明“工具真的被执行了、执行后返回了什么”。在工具函数内部打印是补上这一段:

LangChain 的 Agent 为什么反复调用同一个工具 / 先打印中间步骤看工具返回值
import json
from langchain.tools import tool

@tool
def query_order(order_id: str) -> str:
    print('[TOOL-IN]', repr(order_id))
    if not order_id:
        return json.dumps({'error': 'order_id 不能为空'}, ensure_ascii=False)
    data = {'order_id': order_id, 'status': 'paid'}   # 换成你的真实查询
    out = json.dumps(data, ensure_ascii=False)
    print('[TOOL-OUT]', len(out), out[:300])
    return out

看这三处就能区分三种常见情况:入参为空时,日志是 [TOOL-IN] '' 或 None,说明模型没把参数填上,问题在提示词或参数 schema,而不在工具;入参正常但 [TOOL-OUT] 是空字符串、空列表或 None 时,说明查询条件、鉴权或数据源这一层没取到东西,模型看到的观察值是空的,很容易再试一次;返回格式不对时,通常表现为返回了一大段自由文本或某种对象的 repr,模型找不到字段名,就会换个参数再调一遍。建议工具统一返回 JSON 字符串,并在 return 前做长度截断,避免返回值过长把关键字段顶出上下文。

核对工具名称、描述与参数结构

参数校验失败被反复重试,是最容易被误判成“模型犯傻”的一类。把工具定义调出来逐项对齐,比反复改提示词更有效:

LangChain 的 Agent 为什么反复调用同一个工具 / 先打印中间步骤看工具返回值
@tool('query_order')
def query_order(order_id: str, status: str = 'paid') -> str:
    '''按订单号查询订单。order_id 为字符串,status 可选 paid/refunded。'''
    ...

参数缺失或类型不符时,一般在校验层就被拦下,错误文本里会带字段名和期望类型,比如 order_id field required、Input should be a valid string 这一类内容。这类文本出现在观察值里之后,模型往往不会换工具,而是把同一个工具用略微不同的参数写法再调一次。需要核对的是:工具名是否大小写不一致或带了空格、描述里有没有写清参数格式、必填字段是否被误设为全部必填。可以先遍历 tools 列表,把每个工具的名称和 args_schema 打出来做清单比对。

设置迭代上限并观察停止位置

迭代上限既是保护,也是判断依据。给一个保守配置示例:

executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=6,
    max_execution_time=30,
    early_stopping_method='force',
)

触到上限时,输出里通常会出现受迭代次数或时间限制而停止的提示,措辞随版本不同,同时 intermediate_steps 的轮数是满的。如果提示出现,且最后几轮的动作与参数几乎一模一样,可以倾向判断为死循环;如果每轮动作不同、参数在逐步收敛,多半是正常的多步推理,此时需要的是放宽上限或把任务拆小,而不是修工具。上限值不需要设得很大,够覆盖一次完整任务即可,设太大反而会让无界调用带来额外开销。

LangChain 的 Agent 为什么反复调用同一个工具 / 先打印中间步骤看工具返回值

把工具报错原文与模型下一句动作对齐看

做法是把两类日志写到同一种时间戳格式,然后按时间升序排在一起看:一类是工具内部打印的入参与异常栈,一类是回调钩子里记录的动作名、参数和观察值。对齐之后,常见的两种序列对应两种结论。

  • 报错之后,下一轮动作仍然是同一个工具、同一个参数(或只改了标点和空格):模型在无视报错继续重试。通常是因为观察值里只给了笼统的错误信息,或系统提示要求必须完成该步骤。可以先把工具返回值改成可读的错误说明,写清缺什么、应该怎么补,并在提示里说明报错时不要原样重试。
  • 报错之后,下一轮换参数、换工具,或者直接给出解释并收尾:模型在正常处理报错,属于规划内的路径,一般不需要干预。

操作上建议先只做观测,别急着改提示词:收集两三个完整的调用序列,确认反复调用是集中在某一个工具还是遍布多个工具,再决定改工具返回值格式还是改提示词。每次只改一处,否则很难判断到底是哪一步起了作用;改完仍然用同一组输入复跑一遍,看中间步骤序列是否真的变短。