凌晨一点十七分,日志面板上的行数还在快速上涨。某个内部知识库助手在连续调用同一个查询工具,明明上一次已经拿到了关键词,却还是反复重置搜索条件。看着那串单调重复的调用记录,我在凌晨的办公室里突然意识到:我根本不是在一个“模型不好用”的问题里打转,而是在调试一个AI Agent的主循环。
那晚之后,我再没把 Agent 当成“会说话的数据库”。它真正让人头疼的,不是因为语言模型偶尔说错话,而是因为工具调用、上下文管理、主循环和停止条件这些核心概念没有组装好。这篇文章想把这次踩过的路,尤其是深夜那一场顿悟里想明白的东西,原原本本写下来。适合谁看?适合正在做 Agent 开发的工程师,也适合那些刚刚把大模型 API 接好、却被“明明有工具却不会用”折腾得想摔键盘的探索者。
1. 深夜日志里,我看到 Agent 卡在了一个死循环
1.1 那个让我崩溃的“正常流程”
先交代项目背景。我在做一个内部客服工单的智能归纳模块,目标很朴素:给 Agent 一个任务,它自己从历史工单里搜索相似反馈,再按故障类型归成表格,最后调用“创建汇总报告”的工具把结果写入系统。
前半天都在处理“模型看不懂工具参数”的小毛病,一切看似正常。到了晚上,我随手丢进去一个测试任务:“把最近两周关于登录报错的反馈整理成汇总报告”。前两轮调用也正常, Agent 先调用了一个搜索工具,拿到几十条工单摘要,然后开始逐条读、逐条归类。
从第 3 轮开始,事情就不对了。Agent 读完一批工单后,没有去生成表格,反而又发起了一次搜索,搜索词是“登录失败 最近两周”。第二次搜索返回的结果字段结构差不多,但它还是没汇总,继续搜索“用户无法登录 记录”。我盯着日志看了几分钟,它就像一台失控的自动售货机,反复投币、反复出货,就是不出最终的结账单。
1.2 日志里暴露出的真正问题
我赶紧把最近 30 步的调用轨迹导出来。每一步都长这样:
- 第 5 步:Agent 输出
tool_search_feedback,参数{"keyword": "登录失败最近两周"} - 第 6 步:工具返回 6 条工单摘要
- 第 7 步:Agent 没有给出汇总,而是又输出
tool_search_feedback,参数{"keyword": "用户无法登录 记录"}
一条路走到黑。模型每轮都在使用工具,每轮都能拿到真实数据,但它就是不停下来做最终判断。更讽刺的是,在第二次搜索返回后,上下文里已经包含了足够多的工单,再往下搜也只是把类似内容换个关键词重查一遍。
我当时的第一反应是“提示词写得不够狠”,于是把系统提示词里的要求改成了“你必须基于已有结果生成报告”,但跑了两遍仍然会出现中途折返。真正让我清醒的,是我把每一步的token 消耗和消息语义拆开来看之后:模型不是不知道要汇总,而是它在每一轮都会认为“信息还不够”,于是继续调用工具来补足。它缺少一个显式判断“什么时候可以停”的机制。
1.3 顿悟发生在一念之间
凌晨两点多,我看着第 19 步调用记录时,脑子里突然闪出一个比喻:这个 Agent 就像一个刚入职的实习生,领导说“你去整理一下反馈”,它就一直去档案室抱资料,每抱一摞回来都觉得自己还没抱完,永远不坐下来写报告。
问题不在“模型不会干活”,而在“没有给 Agent 定义完成条件”。模型确实会推理,但推理并不自动导向“停止行动”。在我原来的设计里,主循环的默认行为是“只要模型返回了工具调用,就继续执行”;没有人告诉它“拿到足够数据后应该输出 final_answer”。这成了整场调试的转折点。
2. AI Agent 的核心概念,用一次调试彻底讲透
2.1 大模型是“推理引擎”,不是会思考的大脑
要理解 Agent,先得纠正一个很容易被带偏的认知:大模型本身不是“大脑”,它更像一个推理引擎。给它一段文字,它按概率生成下一段文字;给它一个任务描述,它输出可能的行动序列。它没有任何持久目标,也不会自己“想要”完成什么。
Agent 这个概念,是把“推理引擎”装进了一个循环结构里,由外部代码不断给它喂历史信息、执行它选择的动作,再把它需要的结果塞回上下文。换句话说,模型负责“出主意”,Agent 框架负责“动手”以及“决定何时收手”。很多初学者把模型当成全知全能的实体,出了问题就拼命改提示词,结果越改越玄学。我这次经历也是一样:模型本身完全知道任务是什么,真正缺的,是外部循环没有把“边界”和“终止条件”约束进去。
你可以把模型想象成汽车发动机,Agent 则是整辆车:发动机决定动力,但方向盘、刹车、仪表盘都在 Agent 框架手里。发动机再好,刹车失灵也注定出事。
2.2 工具调用:Agent 从“会想”到“会做”的关键接口
工具调用是 Agent 最核心的接口能力。模型本身不知道你的数据库在哪、不知道外部系统长什么样,它只能输出一段结构化 JSON,声明“我要调用某某函数,参数是什么”。这一步很多人一开始会觉得抽象,其实是整个 Agent 设计的精髓。
具体来说,每个工具需要提供:
- 工具名称:机器执行的唯一标识
- 工具描述:给模型看的,越准确越好
- 参数结构:定义参数名、类型、是否必填
我曾经用的搜索工具定义大致长这样:
{ "name": "search_feedback", "description": "根据关键词搜索用户反馈记录,返回最多 6 条匹配工单摘要", "parameters": { "type": "object", "properties": { "keyword": { "type": "string", "description": "搜索关键词" }, "limit": { "type": "integer", "description": "返回条数上限,默认 6" } }, "required": ["keyword"] } }定义好工具后,Agent 循环里要做的事情就是:把工具定义拼进请求,让模型在需要时输出工具调用指令,再由外围代码真正去执行函数。那晚我犯的错,是把工具描述写得太宽泛,模型误以为“只要还没穷举完所有关键词,就应该继续搜”,于是一个晚上都在这套 JSON 里打转。
2.3 上下文窗口:工作台的大小,决定 Agent 的视野
另一个核心概念是上下文窗口。大模型的输入空间是有限的,通常是几千到几万个 token。在 Agent 循环里,所有历史消息、工具返回、系统提示词都会占用这个空间。它很像一张工作台:桌子越大,能摊开看的材料越多;但不管多大,总有塞满的时候。
我那个“搜索-读-再搜索”的死循环,背后还藏着一个上下文问题:每一次工具返回都新增几百个 token 的工单摘要。模型读到的历史越积越多,它会倾向于相信“还有更多未读材料”,而不是“我手上这批已经够了”。这不是玄学,是模型在大量新输入面前的自然行为——新信息会主导它的注意力,旧目标描述反而被挤出有效视野。
在做 Agent 设计时,得时刻考虑上下文的管理策略。我当时做了一个粗糙但有效的做法:不是把所有搜索结果原样塞回去,而是先让一个后处理函数把每份工单压缩成“编号 + 一句话摘要 + 故障类型标签”,再放回消息历史。这样既保留关键信息,又避免工作台被原始长文本占满。
2.4 主循环:Agent 能连续工作的真正载体
把上面这些串起来,就形成 Agent 真正的主循环。伪代码大概长这样:
def run_agent(user_task, tools, max_steps=10): messages = [{"role": "user", "content": user_task}] for step in range(max_steps): response = llm.chat(messages, tools=tools) if response.finish_reason == "stop": # 模型认为可以结束,输出最终回答 return response.content if response.tool_calls: # 模型要求调用工具 messages.append(response) for tool_call in response.tool_calls: result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) continue # 既不结束,也不调用工具,属于异常情况 # 可以记录日志,并考虑重试或提前终止 break raise MaxStepError("超过最大步数")我过去对 Agent 的一切困惑,最后都被这一百行代码击穿了。模型在循环里扮演“决策者”,框架负责“执行者”,而终止条件、最大步数、异常处理才是让整个系统不脱轨的刹车片。那晚上我反复搜索,本质上就是因为循环体里少了一个“判定信息是否足够”的环节。
3. 那晚踩过的三个坑,也是大多数 Agent 翻车的重灾区
3.1 坑一:工具参数缺失,模型反复“补作业”
第一个坑发生在我加工具参数的时候。我给“创建汇总报告”工具增加了一个必填参数tags,但忘记在工具描述里写清楚它的取值范围。结果模型调用时要么漏传这个参数,要么传一个完全不符合预期的值,工具执行端抛参数校验错误。
更麻烦的是,错误信息会被放回上下文,模型看到报错后会尝试“再修一次”。如果工具一直报错,它就一直重修,直到把整个上下文都堆满报错日志。那场景跟一个倒霉接线员反复拨错号码,每次听到“您拨打的号码为空号”就重拨一遍,绝不换思路。
教训其实很简单:工具的描述和参数校验一定要共同设计。描述只写给模型看,校验则是真实拦截;两边不一致,Agent 就会被错误信息牵着鼻子走。我在工具执行层里加了清晰的中文错误码,并且让模型能看到“哪个参数错了、合法取值是什么”而不是只看到一堆 Python 堆栈。
3.2 坑二:上下文污染,Agent 被工具结果带偏
第二个坑很隐蔽:有一次搜索工具返回的结果文本里包含“看起来需要进一步搜索”这类字样。这本是工单原文里的一句话,但模型读完工具结果后,把这句话当成了对自己的指令,于是又发起一轮新搜索。
这就是上下文污染。工具返回的数据是“资料”,不是“指令”,但模型并不会自动区分。尤其是当工具结果和系统提示词的语义边界模糊时,模型极容易把资料里的话当成新任务。后来我给所有工具输出加了固定包裹结构,开头统一标记这是工具返回的数据,不是指令,并且把数据压成只保留字段的内容。这一改,反复触发搜索的比例立刻降下来。
调试的时候要养成一个习惯:把每一轮的完整消息历史导出来,看看模型是基于哪一句话做出工具调用决策的。这个问题很难靠“改模型能力”解决,只能靠“让数据不做指令”的流程规范。
3.3 坑三:没有停止条件,Agent 永远在“再加一轮”
第三个坑就是开场那次演出:模型要工作时停不下来。原设计里,我只想着“让模型多使用工具”,但没有给它一个明确的“任务完成判据”。于是模型每次拿到新的搜索摘要,都会觉得“再搜一次能更全面”。
解法也很直接,我在任务提示里加了一个硬标记:
当你已经检索到足够支撑结论的材料,直接输出最终报告,不要继续搜索。
并且在主循环里加了分数阈值:如果模型连续三轮都输出同一工具调用,且参数相似度超过 90%,就直接截断,让一个预设结果去接管。这个启发式虽然粗,但是能保命。
停止条件并不是靠“模型自觉”实现的,而是要分成三层:提示词里的目标描述、循环体里的最大步数、以及异常行为检测规则。缺一层,Agent 都可能变成一台轰隆作响但没有产出的机器。
4. 从黑盒到白盒:我的夜间调试工具箱
4.1 每次调用都给同一个 trace_id
夜里能快速定位问题,靠的是一次不落的链路跟踪。我在 Agent 入口处生成了一个trace_id,每次调用模型、每次工具执行、每次把结果写回上下文,都连带记录这个 id。日志字段至少包含这几类:
- 步骤序号与时间戳
- 本轮 messages 数量与总 token 数
- 模型返回内容和工具调用 JSON(完整记录)
- 工具执行结果摘要与错误信息
有了这些数据,我才能在第二天早晨从容地把那个死循环复现出来。没有 trace_id,所有问题都会变成“偶尔跑飞”,根本没法查。
4.2 用“轨迹回放”判断每次选择的依据
日志记录解决了“发生了什么”,但没解决“为什么会这样”。我的做法是把整个调用轨迹导出成可读的剧本:按顺序列成表格,每个步骤一行,后面标注“这一步模型看到了哪几条证据,最终决定调用了哪个工具”。
深夜那天,我正是通过这种回放发现,模型在第 9 步之后其实已经看到了 12 条相关工单,可它仍然选择继续搜索。问题不是“证据不足”,而是“证据已经足够,但没有被设计成判断依据”。这类问题只有在轨迹回放里才能看清,单看最终输出完全看不见。
生产环境不可能每次人工看轨迹,所以我在框架层加了“决策摘要”:每一步记录模型原始输出、工具调用参数、工具结果摘要,让它们可以在调试面板上一屏看完。这个习惯帮我省下至少一半的排障时间。
4.3 准备一个 20 条任务的回归测试集
那晚后我做的第一件事,是整理了一个固定的回归测试集,大概 20 条任务,覆盖搜索、工具参数缺失、信息不足、多轮归纳等场景。每条任务都设定了明确通过标准,例如:
- 必须成功调用创建报告工具
- 工具调用次数不能大于 5
- 报告必须包含至少 3 个故障类型标签
每改一次提示词、每动一次工具定义,就把这 20 条任务整体跑一遍。实测下来,这个回归集的价值比任何大模型技巧都高。Agent 系统最大的风险是“改了这里,坏了那里”,没有一套客观测试,晚上全凭运气。
5. 顿悟后的认知:把 Agent 当成“实习生”来带
5.1 目标、边界和工具,缺一个都带不动
那晚的顿悟如果用一句话说,就是:Agent 不是大脑,而是需要被带教的实习生。带实习生,得给三样东西:
- 明确的目标(要完成什么,做到什么程度算完)
- 明确的边界(哪些工具可用,哪些不该碰)
- 明确的监督(超过步数、重复调用、报错时的兜底方案)
我那晚缺了“做到什么程度算完”,于是模型无限搜索;也缺了“连续重复调用要拦截”,于是循环一路狂奔。把这三样补上之后,这个内部知识库助手的稳定性明显上了一个台阶,它才真正从演示 Demo 变成能用的系统。
5.2 能确定性完成的事,就别让 Agent 自由发挥
另一个很重要的认知是:Agent 的核心价值在“决策”,不在“执行”。如果一个流程可以用几行确定性代码完成,比如查固定接口、拼接固定格式、走固定校验,那就坚决不用模型去“智能判断”。
我当时一度想让模型负责所有的分支选择,结果就是问题复杂、不可复现。后来改成了“规则定义主干流程,模型在关键分叉点做判断”的混合架构:能写if-else的地方全部写死,模型只在需要理解自然语言意图、选择搜索词、判断结果是否充分的地方介入。系统立刻变得更可控,测试成本也降下来。
5.3 Agent 与工作流的选型判断
经历这次之后,我对“什么时候用 Agent”有了更实在的判断标准。如果任务步骤固定、输入输出结构明确,应该先做工作流,也就是用流程编排控制每一步。如果任务需要动态规划、需要根据中间结果不断调整策略,才值得引入 Agent。
我现在的默认策略是:优先把能确定的部分抽成工作流,只在语义决策点使用大模型。比如“把用户反馈归类到故障类型”属于语义判断,让 Agent 做;“拿到归类结果后写入数据库并生成报表”属于确定操作,交给代码做。这样既保留灵活性,又避免模型在简单路径上把复杂问题搞复杂。
6. 留在深夜的最后一句话
那天凌晨收尾时,我在调研笔记里写了一句:“Agent 最难的不是让模型学会用工具,而是让系统知道什么时候该停。”
后来我养成了一个习惯:每次上线 Agent 改动,都会把它塞进那个有 trace_id、有轨迹回放、有最大步数的调试环境里跑一遍回归集。这已经不是我一个人的经验问题,而是团队里谁接手都能快速上手的工程底线。如果你也在做 Agent,我建议先把“能不能看见每一步决策”这件事落实,再谈提示词模板怎么优化。
那晚我盯着日志看到崩溃的时候,以为是自己不懂大模型;现在回头看,真正的问题在于我没有把“推理、工具、上下文、循环”这四个词当成一套需要整体设计的系统。等你在某个深夜里也经历一次“顿悟”,你会明白:AI Agent 真正的工作量,不在模型,而在你把它包起来的那层壳。