1. 从“会调API”到“能设计决策”:Agent开发与算法进阶的底层逻辑
很多人第一次接触 Agent,都是从“让大模型调用一个工具”开始的。写个函数查天气、查数据库、发邮件,然后把它塞进 prompt 里,模型返回一个 JSON,程序解析后执行,再把结果喂回去。这套流程跑通之后,你会觉得自己已经“会 Agent 开发”了。但真正进入复杂场景——比如让 Agent 自主规划一个多步骤任务、在几十个工具里选对那一个、遇到错误能自己回滚重试——你会发现,之前那套“if-else + prompt”的写法根本撑不住。
这就是 Agent 开发和 Agent 算法之间的分水岭。开发解决的是“怎么把模型和工具连起来”,算法解决的是“连起来之后,怎么让它在不确定环境里做出靠谱的决策”。前者是工程问题,后者是策略问题。我见过太多项目,Demo 阶段惊艳,一上生产就崩,核心原因就是只做了开发,没做算法层面的设计。
这篇文章想聊的,就是这条进阶路径上真正卡人的几个环节:Agent 的决策循环到底该怎么设计、工具选择背后的算法逻辑是什么、Benchmark 该怎么看才不被带偏、以及从零搭一个可用的 Agent 项目时,哪些坑是几乎一定会踩的。不管你是刚写完第一个 function calling 的开发者,还是已经在调多 Agent 协作的老手,下面这些内容应该都能对上你遇到过的某些问题。
2. Agent 开发的第一道坎:决策循环不是 while(true) 那么简单
2.1 ReAct 循环的真实开销与终止条件设计
几乎所有 Agent 框架的默认决策循环都是 ReAct 模式:Reason(推理)→ Act(行动)→ Observe(观察)→ 再 Reason。看起来很简单,但实际跑起来,第一个问题就是“什么时候停”。
我早期做的一个内部知识库 Agent,任务是根据用户问题检索文档并生成回答。最初的设计很粗暴:让模型自己判断“我是否已经能回答”,如果能就输出最终答案,否则继续调工具。结果模型经常陷入两种极端:要么检索一次就急着回答,答案残缺;要么反复检索相似内容,五六轮之后还在“我再确认一下”。
后来我改成了显式的终止条件组合:
- 最大轮次硬限制:根据任务复杂度设定,一般 5-8 轮,超过就强制输出当前最优答案
- 信息增益判断:如果新一轮检索返回的内容与上一轮高度重叠(用简单的文本相似度判断),直接终止
- 置信度阈值:让模型在每轮输出一个 0-1 的置信度,低于阈值继续,高于阈值停止
注意:不要完全依赖模型自己判断“我够了”。模型有很强的“过度自信”倾向,尤其是在它已经生成了一段看起来合理的文本之后。硬性规则和模型判断要结合使用。
这里涉及一个容易被忽略的成本问题。每一轮 ReAct 循环都意味着一次完整的 LLM 调用,输入 token 会随着历史记录不断累积。一个 8 轮的循环,如果每轮平均输入 3000 token,总消耗就是 24000 token 左右,成本是单次调用的 8 倍。所以在设计循环时,轮次预算和token 预算要同时考虑。
2.2 工具选择的暴力枚举与剪枝策略
当 Agent 只有 3-5 个工具时,把所有工具描述塞进 prompt 让模型选,完全没问题。但当工具数量到 20 个、50 个的时候,prompt 会变得极长,模型的选择准确率也会明显下降。
我试过几种方案,这里做个对比:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 全量枚举 | 所有工具描述都放进 prompt | 实现简单,无额外依赖 | token 消耗大,选择准确率随工具数下降 | 工具数 < 10 |
| 分类剪枝 | 先让模型选工具类别,再在类别内选具体工具 | token 减少 60% 以上,准确率提升 | 需要维护类别体系 | 工具数 10-50 |
| 向量检索 | 用 query 向量检索最相关的工具描述 | 扩展性好,支持动态添加工具 | 需要 embedding 服务,可能漏选 | 工具数 > 50 |
| 混合方案 | 向量召回 Top-K + 模型精选 | 兼顾效率和准确率 | 实现复杂度最高 | 生产环境推荐 |
我目前最常用的是混合方案:先用向量检索召回 8-10 个候选工具,再让模型从中选择。这样既控制了 prompt 长度,又保留了模型的判断能力。实测下来,在 40 个工具的场景下,选择准确率从全量枚举的 72% 提升到了 91%。
2.3 状态管理与上下文压缩的实操细节
Agent 跑多轮之后,上下文会变得非常长。直接截断会丢失关键信息,全部保留又会爆 token。我的做法是分层管理:
- 系统层:工具定义、角色设定,始终保留
- 任务层:当前任务的原始描述和关键约束,始终保留
- 历史层:每轮的行动和观察结果,按重要性压缩
- 临时层:当前轮的推理过程,轮次结束后丢弃
历史层的压缩策略是:每轮结束后,让模型用一句话总结“这轮做了什么、得到了什么关键信息”,只保留这个总结,原始的工具返回结果如果超过 500 token 就截断或摘要。这样下来,一个 10 轮的 Agent 任务,上下文可以控制在 4000 token 以内。
3. Agent 算法的核心:从规则驱动到学习驱动的演进路径
3.1 为什么纯 Prompt 工程撑不起复杂 Agent
Prompt 工程的本质是“用自然语言写规则”。当规则数量少、边界清晰时,它很有效。但 Agent 面对的环境是不确定的:工具可能返回错误、用户输入可能模糊、任务目标可能中途变化。用 prompt 去覆盖所有这些分支,最终会变成一个几千字的“超级 prompt”,维护成本极高,而且模型对长 prompt 中后段规则的遵循度会下降。
这就是为什么需要引入算法思维。算法思维的核心不是“写更多规则”,而是“设计一个能从反馈中调整策略的机制”。最简单的例子:Agent 调用某个工具失败了,纯 prompt 方案可能会写“如果失败就重试”,但重试几次?间隔多久?换不换工具?这些用 prompt 很难精确控制,用代码逻辑却很容易实现。
3.2 基于反馈的工具调用重试与降级策略
我在实际项目里总结了一套工具调用的容错流程,直接上伪代码:
def call_tool_with_fallback(tool_name, params, max_retries=3): for attempt in range(max_retries): result = execute_tool(tool_name, params) if result.success: return result if result.error_type == "rate_limit": time.sleep(2 ** attempt) # 指数退避 elif result.error_type == "invalid_params": params = repair_params(params, result.error_msg) elif result.error_type == "tool_unavailable": fallback = find_fallback_tool(tool_name) if fallback: return execute_tool(fallback, params) return ToolResult(success=False, error="all_retries_exhausted")这套逻辑的关键在于错误分类。不是所有失败都值得重试:参数错误重试没用,需要修复参数;限流需要等待;工具不可用需要降级。把错误类型判断清楚,重试策略才能有的放矢。
实操心得:在工具返回的错误信息里,尽量让工具本身携带结构化的错误类型字段,而不是让 Agent 去解析自然语言的错误描述。这能大幅提升容错逻辑的可靠性。
3.3 多 Agent 协作中的通信协议与冲突消解
当任务复杂到单个 Agent 搞不定时,就会引入多 Agent 协作。常见的模式有:
- 主从模式:一个 Orchestrator Agent 负责拆解任务,多个 Worker Agent 执行子任务
- 辩论模式:多个 Agent 对同一问题给出方案,通过投票或辩论选出最优
- 流水线模式:Agent 按顺序处理,前一个的输出是后一个的输入
我踩过最大的坑是在主从模式里,Worker Agent 之间的输出格式不统一,导致 Orchestrator 解析失败。后来定了一套简单的通信协议:所有 Agent 之间的消息必须包含task_id、status、result、confidence四个字段,result必须是结构化 JSON。这样 Orchestrator 可以统一处理,也方便做冲突消解——当两个 Worker 给出矛盾结果时,选 confidence 更高的那个,或者触发一次额外的验证调用。
4. Benchmark 的正确打开方式:别被榜单牵着走
4.1 Agent 评测和 LLM 评测的本质区别
LLM 评测通常是单轮问答:给一个问题,看回答对不对。Agent 评测要复杂得多,它评的是一个决策序列的质量。同一个任务,Agent 可能走了 3 步完成,也可能走了 8 步完成,还可能中途失败但最终补救成功。怎么定义“好”?
目前主流的 Agent Benchmark 大致分几类:
| Benchmark 类型 | 代表 | 评测重点 | 局限性 |
|---|---|---|---|
| 任务完成率 | AgentBench | 最终目标是否达成 | 忽略过程效率 |
| 步骤效率 | 自定义 | 用了多少步、多少 token | 难以标准化 |
| 工具调用准确率 | ToolBench | 选对工具、传对参数的比例 | 场景偏简单 |
| 鲁棒性 | 扰动测试 | 遇到错误时的恢复能力 | 构建成本高 |
我的建议是:不要只看一个榜单的排名。公开榜单上的任务往往经过精心设计,和你的实际场景差距很大。更靠谱的做法是,从你的业务里抽 20-30 个真实任务,构建一个私有评测集,每次改动后跑一遍,看完成率和平均步数的变化。
4.2 构建私有评测集的实操步骤
- 任务采样:从历史日志里抽取真实用户请求,覆盖高频和边缘场景
- 标准答案标注:人工标注每个任务的预期结果和可接受的工具调用路径
- 自动化评分:写一个评分脚本,检查最终结果是否正确、工具调用是否合理
- 回归测试:每次修改 Agent 逻辑后,跑全量评测集,对比指标变化
这个过程听起来麻烦,但一次投入之后,后续每次迭代都能省下大量人工验证的时间。我自己的项目里,私有评测集从最初的 15 个任务扩展到了 80 多个,覆盖了 90% 以上的线上场景,迭代效率提升了至少三倍。
4.3 从 Benchmark 结果反推优化方向
跑完评测后,重点看的不是总分,而是失败案例的分布。我通常会把失败分成几类:
- 工具选择错误:模型选了不相关的工具 → 优化工具描述或引入剪枝
- 参数传递错误:工具选对了但参数不对 → 检查参数 schema 是否清晰
- 循环终止失败:该停的时候没停 → 调整终止条件
- 上下文丢失:关键信息在压缩时被丢弃 → 调整压缩策略
每一类失败对应不同的优化手段。如果只是笼统地看“完成率 75%”,你根本不知道从哪里下手。但如果你知道“75% 里有 12% 是工具选择错误”,优化方向就非常明确了。
5. 从零搭建 Agent 项目的完整实操记录
5.1 技术选型:框架还是裸写
这是每个 Agent 项目开始时都会纠结的问题。我的经验是:
- 学习阶段:用 LangChain 或类似框架,快速理解 Agent 的基本概念和流程
- 原型阶段:框架 + 自定义逻辑混合,框架负责基础编排,自定义逻辑处理业务特殊需求
- 生产阶段:逐步替换框架依赖,核心决策循环自己实现
原因很简单:框架的抽象层在简单场景下很方便,但在复杂场景下会成为限制。比如你想自定义一个特殊的终止条件,或者想控制每轮 prompt 的具体构造,框架往往不给你这个灵活性。而且框架的版本更新可能引入不兼容变更,生产环境里这是个风险。
我现在的做法是:用框架做快速验证,验证通过后,把核心逻辑用 200-300 行代码重写一遍。这样既享受了框架的开发效率,又保证了生产环境的可控性。
5.2 核心模块拆解与代码结构
一个可用的 Agent 项目,我通常会拆成这几个模块:
agent_project/ ├── core/ │ ├── loop.py # 决策循环 │ ├── planner.py # 任务规划 │ └── executor.py # 工具执行 ├── tools/ │ ├── registry.py # 工具注册与检索 │ └── implementations/ # 具体工具实现 ├── memory/ │ ├── short_term.py # 短期上下文管理 │ └── long_term.py # 长期记忆(向量库) ├── evaluation/ │ ├── benchmark.py # 评测脚本 │ └── tasks/ # 评测任务集 └── config/ └── settings.py # 模型、工具、循环参数配置这个结构的好处是职责清晰。loop.py只管循环逻辑,不关心工具怎么实现;registry.py只管工具的注册和检索,不关心循环怎么跑。改任何一块都不会影响其他部分。
5.3 关键参数的计算与调优过程
Agent 项目里有几个参数几乎一定要调:
最大循环轮次:我的经验公式是max_rounds = ceil(任务平均步骤数 × 1.5)。比如你的任务平均需要 4 步完成,那 max_rounds 设 6 比较合适。设太小会导致复杂任务完不成,设太大浪费 token 且增加出错概率。
工具召回数量:如果工具总数是 N,召回数量 K 一般取min(10, ceil(N × 0.3))。比如 40 个工具,召回 10 个;10 个工具,召回 3-5 个就够了。
上下文压缩阈值:当历史 token 超过模型上下文窗口的 60% 时触发压缩。比如模型支持 8K 上下文,那超过 4800 token 就开始压缩历史层。
这些参数没有绝对的最优值,需要根据你的具体场景跑评测来调。但有了初始值,调优的方向会清晰很多。
5.4 上线前的检查清单
在把 Agent 推到生产之前,我每次都会过一遍这个清单:
- [ ] 最大循环轮次和 token 预算是否设置
- [ ] 所有工具是否有超时和重试机制
- [ ] 工具调用失败时是否有降级方案
- [ ] 上下文压缩是否会丢失关键信息(用长任务测试)
- [ ] 私有评测集完成率是否达标
- [ ] 异常情况下的日志是否足够排查问题
- [ ] 是否有 rate limit 保护,防止 Agent 疯狂调用
特别提醒:一定要做 rate limit 保护。我见过一个 Agent 因为循环终止条件写错,在 10 分钟内调用了 2000 多次 API,账单直接爆炸。加一个简单的计数器,超过阈值就强制终止,能避免这种灾难。
6. 常见问题与排查技巧实录
6.1 Agent 陷入死循环怎么办
这是最常见的问题。表现是 Agent 反复调用同一个工具,或者在不同工具之间来回切换但任务没有进展。
排查思路:
- 先看日志,确认循环是在哪一步开始重复的
- 检查终止条件是否被正确触发
- 检查工具返回结果是否包含足够的信息让模型判断“下一步该做什么”
- 如果工具返回空结果或错误,模型可能会一直重试同一个工具
解决方法:加入“重复检测”机制。记录最近 3 轮的工具调用和参数,如果发现高度重复,强制终止或切换策略。
6.2 工具调用参数总是传错
模型传错参数通常有两个原因:工具的参数 schema 描述不清,或者模型对参数格式理解有误。
我的做法是:
- 在工具描述里给出完整的参数示例,而不只是类型说明
- 对枚举类型的参数,把所有可选值列出来
- 如果参数有格式要求(如日期格式),在描述里明确写出
比如不要写“date: 日期”,而要写“date: 日期,格式 YYYY-MM-DD,例如 2024-01-15”。
6.3 上下文太长导致模型“失忆”
当上下文超过一定长度后,模型对早期信息的注意力会下降。表现是 Agent 忘记了任务目标,或者忘记了之前已经获取的信息。
解决方法除了前面说的分层压缩,还有一个技巧:在每轮 prompt 的开头,用一句话重申当前任务目标。这看起来很简单,但实测能显著降低“跑偏”的概率。
6.4 多 Agent 之间消息丢失或格式错误
多 Agent 协作时,消息传递是最容易出问题的环节。我的经验是:
- 所有 Agent 之间的消息必须经过一个消息总线,而不是直接互相调用
- 消息总线负责格式校验,格式不对的直接拒绝并返回错误
- 每个消息带唯一 ID,方便追踪
这样即使某个 Agent 输出了格式错误的消息,也不会污染整个系统,排查起来也容易定位。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 循环不终止 | 终止条件太宽松 | 看日志确认轮次 | 加硬性轮次限制 |
| 工具选错 | 工具描述不清 | 检查 prompt 中的工具定义 | 优化描述或加剪枝 |
| 参数错误 | schema 不明确 | 看模型输出的参数 | 补充示例和格式说明 |
| 上下文丢失 | 压缩过度 | 对比压缩前后的信息 | 调整压缩策略 |
| 响应变慢 | 轮次过多或工具超时 | 看每轮耗时 | 优化循环或加超时 |
| 成本过高 | token 消耗大 | 统计每轮 token | 压缩上下文、减少轮次 |
7. 进阶方向:Agent 算法的下一步在哪里
7.1 从手工规则到学习型策略
目前大多数 Agent 的决策逻辑还是手工设计的:什么时候重试、什么时候切换工具、什么时候终止,都是人写的规则。下一步的进化方向是让 Agent 从历史交互中学习这些策略。
最简单的切入点是工具选择的偏好学习:记录每次工具调用的结果,成功的选择给正反馈,失败的给负反馈,逐渐调整工具选择的优先级。这不需要复杂的强化学习,一个简单的 bandit 算法就能见效。
7.2 Agent 与 LLM 能力边界的协同演进
Agent 的能力上限受限于底层 LLM 的能力。当 LLM 的推理能力提升时,Agent 的规划能力也会跟着提升。但反过来,Agent 的算法设计也可以弥补 LLM 的某些不足。比如 LLM 不擅长精确计算,那就把计算交给工具;LLM 不擅长长期记忆,那就用外部存储来补充。
理解这个协同关系,能帮你在设计 Agent 时做出更合理的分工:哪些交给模型,哪些交给代码,哪些交给工具。
7.3 评测驱动的持续迭代闭环
最后想强调的是,Agent 项目的迭代一定要有评测驱动。没有评测,你就是在盲调。有了评测集,每次改动都能量化效果,迭代方向也会清晰很多。
我自己的项目里,评测集是持续更新的:每次发现线上有新的失败案例,就把它加入评测集。这样评测集越来越贴近真实场景,Agent 的表现也越来越稳。
这个闭环跑通之后,Agent 的优化就从“凭感觉”变成了“看数据”,效率完全不一样。