这两年聊 AI Agent 的文章,基本都在解释“Agent 是什么”:能拆任务、能调工具、能自己决策。可真到自己上手做工程实现,你会发现概念层面的热闹撑不住代码层面的冷清——Demo 里那个会自己刷网页、写周报的 Agent,挪到生产环境之后,不是上下文爆掉,就是工具调一半卡死,再不然就是在同一个错误上反复横跳。问题不是出在大模型不够聪明,而是出在“Agent 工程”这件事本身还没被结构化地讲清楚。
我这篇文章想做的事,就是把我自己折腾 Agent 工程这些年的总结,拆成两个可以落地的观察框架:一个是七要素,用来回答“一个 Agent 系统里到底有哪些组成部分”;另一个是七个决策点,用来回答“从 Demo 走到生产环境时,每一步选型该怎么取舍”。两个框架合在一起,就是一条从概念到代码的完整路径。适合正在做 Agent 开发、被各种架构图和名词绕晕的工程师,也适合想评估 Agent 落地成本的技术负责人。
1. 七要素是拆“组件”,七个决策点是过“关卡”:Agent 工程的两个观察框架
先说清楚为什么需要两套框架,而不是一套。
你去看市面上各种 Agent 架构图,大多把 Agent 画成一个圆,中间写“LLM”,周围挂几个小方块:Tools、Memory、Prompt、Action。这种图看完之后你会觉得“好像懂了”,但落笔写代码时不知道从哪开始。因为架构图是“静态结构”,而 Agent 最重要的是“动态行为”——模型怎么循环调用工具、怎么在失败时修正方向、记忆怎么在每一轮之间流转。七要素解决的是前者,七个决策点解决的才是后者。
1.1 七要素拆的是“Agent 由哪些零件组成”
我习惯把 Agent 拆成七个要素,顺序按从核心到外围排:
- 模型(Model):决策大脑,负责理解目标、生成计划和执行动作。
- 规划(Planning):把大目标拆成子任务、确定执行顺序的能力。
- 工具(Tools):Agent 对外部世界施加影响的接口,比如搜索、发请求、操作数据库。
- 记忆(Memory):短期上下文窗口和长期外部存储的统称。
- 执行器(Executor):把模型的决策转成真实操作的那一层,通常表现为代码里的一个循环。
- 感知与反馈(Observation & Feedback):读取工具返回结果、环境状态和用户输入,作为下一轮决策的依据。
- 自我改进(Self-improvement):从错误中总结、修正计划甚至调整 prompt 的环节。
这七个要素几乎对应着 Agent 运行时的每一个环节。后面第 2 节我逐个说透。
1.2 七个决策点卡的则是“具体选哪条路”
要素是“必须有”,决策点是“怎么选”。同样是具备七要素的系统,模型用云 API 还是本地部署、工具用 Function Calling 还是 MCP、记忆用滑动窗口还是向量库……这些选择直接决定系统的成本、稳定性和复杂度。
七个决策点,对应从设计到上线会反复纠结的七个问题:
- 模型选型与部署形态:通用模型还是专用模型?云 API 还是本地?上下文要多大?
- 记忆与上下文策略:窗口怎么管理、过期信息怎么淘汰、长期信息怎么检索。
- 工具接入方式:Function Calling 直连还是走 MCP 协议?超时和幂等怎么做?
- 单 Agent 还是多 Agent:一个大脑包揽全局,还是拆成多个角色分工协作。
- 执行模式:同步阻塞、异步任务、事件驱动还是批处理。
- 可观测性与质量评估:怎么判断 Agent 到底干得好不好,出错之后能不能追溯。
- 成本、延迟与并发控制:Token 消耗怎么压、并发上限怎么定、模型降级怎么做。
1.3 两个框架的衔接关系
要素决定“你缺什么”,决策点决定“你填什么”。一个典型的推演过程是这样的:先按七要素盘一遍系统——模型定了、工具注册表有了、记忆模块建了、反馈链路接上了;然后逐个过七个决策点——模型不够快就换供应商、工具调用不稳定就加超时重试、上下文膨胀就切记忆策略、Agent 行为不可控就上 trace 和评估集。七要素让你看得清全局,七个决策点让你走得动泥潭。下面两节分别展开。
2. 七个要素逐一说透:大脑、手脚、记忆和反思在工程里到底是什么
这一节不讲概念,讲每个要素在代码里的实体和边界。你读完能拿着这张对照表去盘点自己的系统缺什么。
2.1 模型要素不只是“调 API”:决定能力上限和成本下限
模型是 Agent 的大脑,但在工程实现里,模型要素包含三个子问题:选哪家模型、用什么版本、怎么调用。
先说选型。做 Agent 不比做聊天机器人,对模型的要求不只是“会说话”,更重要的是指令遵循能力和工具调用稳定性。同一个任务,有的模型能准确地输出{"name": "search_web", "arguments": {...}},有的模型会在 JSON 里多塞几句解释,导致解析直接失败。因此我建议你在选型时准备一组工具调用压力测试集——100 条需要调用 2 到 3 个工具才能完成的任务,测成功率、测解析率、测平均 Token 消耗。
再说调用。模型要素在工程上就是一段 API 封装,但这层封装很关键,它决定了下游所有环节怎么对接。我的做法是只定义两种接口:
chat(messages, tools, ...):用于规划决策,模型可能返回文本,也可能返回工具调用指令。complete(prompt):用于补全、摘要、重写这类子任务。
封装的内部处理好重试、超时、模型版本切换和流式输出。这层做稳了,后面换供应商、换模型参数都不会伤筋动骨。
2.2 规划要素:Plan-and-Execute 与 ReAct 两种风格的取舍
规划要素决定 Agent “怎么想”。工程上常见的思路有两种:
- ReAct 风格:模型走“思考(Thought)→ 行动(Action)→ 观察(Observation)”的循环,走一步看一步,灵活性高,但 Token 消耗大,长任务容易在中间跑偏。
- Plan-and-Execute 风格:先让模型生成一个完整计划,再按计划逐步执行,每执行完一步可以重新审视计划。这种风格适合任务链路比较确定的场景,比如“查数据→生成图表→写报告”这种三步走的流程。
我现在的习惯是:默认走 Plan-and-Execute,但允许在执行循环里出现“计划修正”分支。也就是说,模型每完成一个子步骤,都可以选择“继续下一步”或“重新规划”。这样既保留了灵活性,又避免了 ReAct 那种每一步都无边界发散的问题。
2.3 工具要素:注册表、参数校验和结果封装构成工具层
工具是 Agent 的“手脚”,但工具的工程实现远比“写一个函数”复杂。一个规范的工具层至少包含三部分:
工具注册表:所有工具函数的元信息统一登记,包括名称、描述、参数 JSON Schema。模型并不直接调用函数,它只是看到工具的“描述”,然后返回一个调用指令;真正执行由系统侧完成。所以工具描述的准确性直接决定模型的调用成功率。
参数校验与转换:模型返回的参数是字符串,要经过一层校验和类型转换才能传给真实函数。别省这一步——模型经常会把枚举值写错、把数字写成字符串。我在工程里都会加一层 Pydantic 或 JSON Schema 校验,校验失败时把错误信息回传给模型,让它自行修正。这个“回传修正”机制比你在系统里硬编码参数修复有效得多。
结果封装:工具执行完返回什么格式给模型,需要统一约定。我通常返回一个结构化 JSON:{"success": true/false, "data": ..., "error": ...}。关键是:千万不要把未做截断的完整结果直接塞回上下文,工具返回 2 万行日志就是上下文灾难。我会自己做一层摘要,只把前几条关键记录和统计摘要回填给模型。
2.4 记忆要素:上下文窗口是工作台,向量库是档案柜
很多新人有一个误解:上下文越大,记忆越强。实际上,上下文窗口只是“工作台”,模型能“看到”的内容不等于模型能“用好”的内容。窗口一大,注意力分散,模型反而容易出现选择困难。
我的记忆分层经验是三者配合:
- 短期记忆:当前对话窗口里的原始消息。窗口接近上限时,触发摘要压缩。
- 工作记忆:当前任务需要持续关注的中间状态,比如计划列表、已执行步骤。这部分必须以结构化数据维护,而不是塞在对话历史里。
- 长期记忆:从历史对话和任务结果里抽取的结论、偏好、事实,存进向量库或 KV 存储,需要时检索后注入上下文。
工程上有一个容易忽略的细节:记忆模块应该显式区分“用户说了什么”和“系统得出了什么”。把模型自己之前的胡说八道存进长期记忆,几天后它会把错误当事实用。我只存“经过工具验证的结果”或“用户明确确认的信息”,模型在 ReAct 过程中的思考过程不进长期记忆。
2.5 感知与反馈要素:工具结果、环境状态和用户修正回路
Agent 如果没有良好的反馈回路,就像一个人蒙着眼睛干活。反馈要素在工程上包含三层:
第一层是工具返回结果,上一节说的结果封装就是这层的一部分。第二层是环境状态,比如页面加载状态、进程是否存活、API 限流余量。第三层是用户修正——用户说“停一下”“换一种思路”,这类指令优先级最高,必须能随时打断 Agent 的自主循环。
这层最容易踩的坑,是误以为“工具返回了内容 = 任务成功了”。很多工具调用失败后返回的是一段错误堆栈,模型也能读;但如果错误信息语义不清晰,模型会把报错误判为正常结果继续往下走。所以反馈链路里要有一个明确的状态字段,模型在做推理时第一眼看success,而不是从一坨文本里猜。
2.6 自我改进要素:Agent 的进化能力藏在评估闭环里
自我改进听起来很玄,但工程实现其实落在一个闭环上:记录失败案例 → 分析失败原因 → 调整 prompt / 工具描述 / 流程 → 回归验证。
我见过最简单的自我改进机制,是给 Agent 循环加一个“事后回顾”步骤:任务结束后,让模型用系统 prompt 反思“这次执行哪里不顺、下次遇到同类任务要怎么调整方向”,把反思结果存成文本。这个做法效果有限,但聊胜于无。真正有效的做法,是形成一套回归评估集:每发现一个失败模式,就把它变成一条评估用例。下一次 Agent 行为变更后,先把回归集跑一遍,看有没有引入新问题。关于评估集的构建,我在第 3 节的可观测性决策点里展开。
3. 七个决策点一关一关过:从模型选型到可观测性的取舍逻辑
七要素搭好了骨架,接下来的是血和肉。七个决策点是工程实现里最消耗心力的部分,每个决策点我都给你一个“怎么选”的方法论。
3.1 决策点一:模型选型与部署形态,先定“类型+阈值”再选供应商
模型选型这个决策,很多团队是被热门榜单带着走的。我建议你建立一个自己的评估维度表,至少包含四项:
| 评估维度 | 核心关注点 | 测试方法 |
|---|---|---|
| 工具调用准确率 | 是否能稳定返回合法、完整的调用指令 | 拿 100 个工具用例打分 |
| 指令遵循能力 | 是否能严格按输出格式、长度、风格约束执行 | 看格式错误率和偏离率 |
| 中文上下文理解 | 长段中文语义捕捉、多轮记忆保持 | 用你业务领域的长文测试 |
| 延迟与成本 | P50/P95 延迟,单任务 Token 消耗 | 真实任务压测 |
部署形态上,云 API 的优势是省事、模型迭代快,缺点是延迟受网络波动影响、成本随用量线性增长。本地部署则适合数据敏感、离线、高并发场景,对 GPU 和推理引擎的运维能力要求更高。我的建议是:项目初期无脑选云 API,先把业务逻辑跑通;当单月成本或延迟指标触到阈值(例如单任务 Token 成本超过毛利预期、P95 延迟超过 3 秒)时,再考虑换模型或加推理缓存。不要前期就自建推理集群。
关于热词里常看到的“token 是什么意思”——简单说,token 是模型处理文本的最小单位,英文大概 4 个字符一个 token,中文差不多 1 到 1.5 个字一个 token。它同时是计费单位和上下文容量单位,Agent 的每一项决策(思考、调工具、看结果)都在消耗 token,所以后面大部分优化本质上是“省 token”。
3.2 决策点二:记忆与上下文策略,用“三层缓存”的思路管窗口
上下文管理是我认为 Agent 工程里最容易被低估的一环。模型上下文是有限资源,Agent 每执行一轮任务,对话历史就会膨胀一轮。策略层面有三种方案可以组合:
- 滑动窗口:只保留最近 N 轮对话,实现最简单,但会丢失早期关键信息。
- 摘要压缩:窗口快满时,把早期对话重写成摘要。实现不复杂,但摘要本身也占 token,且压缩过程有信息损失。
- 向量检索:把历史信息切块向量化,需要时按相似度召回。信息密度高,但引入了相似度计算和检索失败的噪声。
我的实践是“三层混合”:按时间衰减把信息分为热层(当前任务上下文,完整保留)、温层(最近任务的摘要,压缩保留)、冷层(历史事实和结论,向量检索后按需注入)。判断触发条件的方法是监听 token 用量和上下文占比,当剩余可用上下文低于总窗口 20% 时触发压缩和归档。这个阈值可以根据模型类型微调,但 20% 是一个比较稳的起点。
3.3 决策点三:工具接入方式,Function Calling 和 MCP 之间不是单选题
工具接入是新老 Agent 系统分水岭最明显的地方。早期做法是开发者手写工具描述、塞进 prompt、解析模型输出,也就是 Function Calling 的原型。后来各家大厂都提供了原生 Function Calling 能力,模型侧保证输出结构稳定、参数能对齐。Function Calling 的优势是成熟、调试简单、延迟低,适合工具数量少(比如 10 个以内)、调用链路由开发者可控的场景。
MCP(Model Context Protocol)出现后,工具接入进入了“标准化”阶段。它的核心思路是把工具接入抽象成统一协议:Agent 主机通过 MCP 客户端连接工具服务器,工具可以提供资源、提示或能力。好处是同一套代码能对接任意支持 MCP 的服务,生态意义上的互操作性强。
但我不建议“All in MCP”。原因很直接:MCP 的重心在于“协议统一”,但当你的业务只有三五个内部 API 时,走协议层反而增加了一层的调试成本、连接管理和网络开销。我的选型标准是:团队自研、调用频率高、延迟敏感的内部工具用 Function Calling 直连;第三方服务、需要跨系统复用、生态更新快的工具走 MCP。另外,工具层无论选哪条路,超时时间、重试策略、幂等设计是必须做的:外部 API 调用要设定超时上限,工具执行最好有任务 ID 来保证重试不产生重复副作用。
3.4 决策点四:单 Agent 还是多 Agent,先看任务的可并行度
“多 Agent 协作”这几年非常火,但工程复杂度也被明显放大了。我的判断标准是三个问题:任务是否需要多个不同领域的模型角色?子任务之间是否有明确的收发接口?是否真的需要并行处理?
如果你的答案是“都不是”,老老实实用单 Agent。单 Agent 的可控性、可调试性和成本优势都非常明显。只有当你遇到“领域隔离 + 环节复杂 + 流量大”三重条件同时满足时,才值得拆多 Agent。比如客服系统里,一个 Agent 负责意图识别、一个 Agent 负责检索知识库、一个 Agent 负责生成回复,这种拆法是为了隔离 prompt 和权限边界,而不是为了炫技。
多 Agent 架构有三个绕不开的工程问题:通信协议(Agent 之间传格式化 JSON 还是自然语言?)、编排拓扑(链式、路由、还是共享黑板?)、状态一致性(Agent 之间共享哪些状态?冲突怎么仲裁?)。这些问题的调试成本是几何级上升的。我的建议是:从单 Agent 起步,当 agent 的 prompt 已经长到无法维护、或者不同步骤的模型需求冲突时,再按边界逐步拆分,而不是一开始就设计五角色协作图谱。
3.5 决策点五:执行模式,决定系统是“能用”还是“扛得住”
Agent 的执行模式直接决定系统的吞吐和稳定性。实体上,我把它分为四种:
| 执行模式 | 适用场景 | 关键工程点 |
|---|---|---|
| 同步阻塞 | 用户在线等待结果,如对话助手 | 超时、流式输出、客户端重试 |
| 异步任务 | 耗时任务,如批量报告生成 | 任务队列、状态查询、回调通知 |
| 事件驱动 | 被动响应触发,如监控告警处理 | 事件订阅、幂等消费、背压控制 |
| 批处理 | 离线清洗、批量长文本处理 | 分片、断点续跑、并发限流 |
大多数业务不是“只有一种模式”,而是在一条链上混合。比如“用户请求 → 同步阻塞生成计划 → 转为异步任务执行 → 完成后事件回调”。工程上要特别注意,Agent 的循环不适合做同步长连接——一次任务可能跑几十轮工具调用,单次请求耗时动辄几分钟,这会拖垮 Web 服务的 worker 池。实践上我会把规划阶段做成同步(快速返回“已开始”),把工具执行阶段移到后台任务队列,再由前端轮询或 WebSocket 推送动态。
3.6 决策点六:可观测性与质量评估,没有度量就没有迭代
Agent 和传统程序最大的差别是:你没法断言“这一行代码执行完是对是错”。所以可观测性不是可选项,而是 Agent 工程质量的生命线。至少要采集三类数据:
轨迹(Trace):完整记录每一次模型调用的输入、输出、工具选择、参数、返回值。注意,这里的输入输出最好做脱敏和截断,不然日志存储量会爆炸。
指标(Metric):任务成功率、平均轮数、工具调用成功率、上下文压缩次数、Token 消耗分布、单任务延迟。这些数字能直接告诉你优化方向。
评估(Evaluation):构建回归集,用大模型或规则给 Agent 输出打分。评分维度可以包括任务完成度、工具调用准确性、格式合规性、用户意图满足度。
这里有一个容易被忽略的事:质量评估必须包含“失败样本归因”。每次任务失败,不仅要记录“失败了”,还要让系统自动保存失败路径的完整 trace,并打上失败原因标签(比如“工具超时”“参数解析失败”“规划偏离目标”)。积累一批标签后,你就能知道最该优先解决的问题是什么。这也是前面说的“自我改进”在工程里的真正抓手。
3.7 决策点七:成本、延迟与并发控制,优化空间藏在模型分级里
最后这个决策点直接决定项目能不能盈利。Agent 的成本控制不像普通接口,因为模型的每次调用都会附带多轮工具上下文放大,Token 消耗可能是指数级的。省钱的核心思路不是压价,而是减少不必要的消耗:
- Prompt 瘦身:系统指令里只保留与当前任务相关的约束,把通用背景知识移到工具调用里按需获取。
- 缓存策略:对相同或高度相似的用户请求命中结果缓存;对工具返回内容做摘要后入缓存。
- 模型分级:意图识别、关键词抽取这类简单任务用便宜的小模型;复杂规划、长文本推理才用大模型。这是成本优化空间最大的一项。
并发控制上要强调的是“限流语义”:不只限 API 调用频率,还要限 Agent 启动的任务数,否则某个用户的一个任务循环 30 轮,可能瞬间打爆供应商并发配额。我会在任务队列里做信号量控制,上限设置为供应商并发额度的 70%,留出余量应对突发。
4. 一份可以落到代码里的工程骨架:从配置管理到工具注册的实现路径
这一节给出一份可以直接抄作业的最小骨架。我以 Python 为例,结合前面七要素和七个决策点里最关键的环节,做一个能跑通的 Agent 循环。你拿去换成自己的工具集和模型参数就能用。
4.1 环境准备与配置外部化
项目结构上我习惯这样分:
agent_workspace/ ├── config/ # 配置文件,禁止硬编码 ├── tools/ # 工具注册与实现 ├── memory/ # 记忆管理 ├── core/ # Agent 循环、模型封装 ├── eval/ # 回归评估集 └── app.py # 启动入口配置项至少包括:模型名、基础 URL、密钥注入方式、上下文上限、单任务最大轮数、工具超时时间。密钥不要出现在代码里,用环境变量或密钥管理服务注入。这一步虽然简单,却是生产环境的第一道门槛。
# config/settings.py from pydantic_settings import BaseSettings class Settings(BaseSettings): model_name: str = "qwen2.5" api_base: str = "https://your-endpoint" api_key: str = "env" # 从环境变量读取 context_limit: int = 32000 max_agent_rounds: int = 15 tool_timeout: int = 10 max_concurrent_tasks: int = 20 settings = Settings()4.2 模型封装:对外只暴露两个方法
模型封装层是所有 Agent 组件的数据枢纽。我建议做两层:底层是“单次调用”,上层是“会话级调用”。会话级调用内部维护消息列表和上下文截断逻辑。
# core/llm.py from openai import AsyncOpenAI from typing import Optional, List, Dict class LLMClient: def __init__(self, settings): self.client = AsyncOpenAI( base_url=settings.api_base, api_key=settings.api_key ) self.settings = settings async def chat( self, messages: List[Dict], tools: Optional[List[Dict]] = None, temperature: float = 0.2, ) -> Dict: """单轮对话,可能返回文本或 tool_calls""" resp = await self.client.chat.completions.create( model=self.settings.model_name, messages=messages, tools=tools, temperature=temperature, ) return resp.choices[0].message async def complete( self, prompt: str, max_tokens: int = 1024, ) -> str: """用于摘要、重写等辅助任务""" resp = await self.client.chat.completions.create( model=self.settings.model_name, messages=[{"role": "user", "content": prompt}], max_tokens=max_tokens, temperature=0.1, ) return resp.choices[0].message.content这里我把temperature默认调到 0.2,因为 Agent 场景下创造性不是刚需,稳定性才是。规划、工具调用这类任务用低温,文案生成这类子任务可以单独调高。
4.3 工具注册表:装饰器 + Schema 自动生成
工具注册表是 Agent 手和脑的接口。我用装饰器把函数信息自动转成模型需要的 JSON Schema,省去手工维护双份定义的工作,也避免“函数更新了、描述忘了改”的不一致问题。
# tools/registry.py import inspect, json from functools import wraps from typing import Dict, List, Callable TOOL_REGISTRY: Dict[str, Dict] = {} TOOL_FUNCS: Dict[str, Callable] = {} def tool(name: str, description: str): def decorator(func): # 用 inspect 自动从函数签名生成参数 schema(简化示例) sig = inspect.signature(func) properties = {} required = [] for param_name, param in sig.parameters.items(): properties[param_name] = {"type": "string"} # 真实项目用 pydantic schema if param.default is inspect.Parameter.empty: required.append(param_name) schema = { "type": "object", "properties": properties, "required": required, } TOOL_REGISTRY[name] = { "type": "function", "function": {"name": name, "description": description, "parameters": schema}, } TOOL_FUNCS[name] = (func, schema) return func return decorator async def call_tool(name: str, args: Dict): """统一工具调用入口:校验参数、执行、封装结果""" func, schema = TOOL_FUNCS[name] try: # 参数校验(真实项目用 pydantic) if not set(schema.get("required", [])).issubset(args.keys()): raise ValueError(f"缺少必要参数: {args}") result = await func(**args) return {"success": True, "data": result} except Exception as e: # 关键:把错误信息回传给模型,让模型自行调整 return {"success": False, "error": str(e)}4.4 Agent 主循环:ReAct 简化版,但加上最大轮数和放弃条件
Agent 主循环是执行器的核心。这里我实现一个带“最大轮数保护”的循环:模型决定调用工具就执行并回填结果,模型决定结束就退出;如果连续多轮没有调用工具也没有结束,触发放弃逻辑。
# core/agent.py from tools.registry import TOOL_REGISTRY, call_tool class AgentLoop: def __init__(self, llm, tools=None, max_rounds=15): self.llm = llm self.tools = tools or list(TOOL_REGISTRY.values()) self.max_rounds = max_rounds async def run(self, user_query: str): messages = [{"role": "system", "content": self.system_prompt()}] messages.append({"role": "user", "content": user_query}) for step in range(self.max_rounds): resp = await self.llm.chat(messages, tools=self.tools) # 模型决定直接返回答案 if not getattr(resp, "tool_calls", None): return resp.content # 模型要求调用工具,逐个执行并回填 messages.append(resp) # 保留 assistant 的 tool_calls 消息 for tool_call in resp.tool_calls: tool_name = tool_call.function.name tool_args = json.loads(tool_call.function.arguments or "{}") tool_result = await call_tool(tool_name, tool_args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": self._format_tool_result(tool_result), }) # 达到最大轮数,强制收尾 return "达到最大轮数,任务未完成。" def _format_tool_result(self, result: Dict, max_chars: int = 2000) -> str: """对工具结果做截断,防止上下文膨胀""" text = json.dumps(result, ensure_ascii=False) if len(text) > max_chars: # 截断并只保留摘要性字段 text = text[:max_chars] + "...[已截断]" return text def system_prompt(self) -> str: return ( "你是一个任务执行助手。你的目标是:理解用户需求,使用可用工具完成任务。" "如果工具调用失败,请根据错误信息修正参数后重试,不要编造成功结果。" "如果已经收集到足够信息或遇到不可恢复的错误,直接输出最终答复。" )这段代码信息量挺大,我逐行解释几个关键点:
messages.append(resp)这一步不能省。模型返回的 tool_calls 必须原样进入下一轮消息列表,否则 API 会报“多轮工具调用缺少上下文”。这是 Function Calling 最容易写错的地方。- 回填的
tool消息必须带tool_call_id,用来和上一步的调用请求对应。 - 工具结果截断是上下文保护的基本功。我在
_format_tool_result里做了字符级截断,更精细的做法是按结构化字段保留枚举摘要和统计结论。 - 失败结果也回填。这个设计刻意为之——让模型看到
{"success": false, "error": "..."},它可能自行修正参数重试,这就是反馈儿子到自我改进的最短路径。
4.5 Rust 做 Agent 的意义:一个被低估但确实存在的高并发视角
热词里出现“基于 Rust 语言 AI Agent”,这里给一个工程上的客观比较。Agent 服务本质上是一个 IO 密集 + 网络密集系统:大量等待外部 API 返回、工具结果、流式传输,真正消耗 CPU 的推理逻辑全部在模型端。这种特征下,Rust 的异步运行时(tokio)确实能提供高并发和低资源占用优势,对内存占用敏感的边缘场景友好。
但对大多数产品和业务团队,Python/TypeScript 生态的开发效率和 AI SDK 集成便利性更实际。我的判断标准是:如果你已经有 Node 或 Python 的存量服务,不要为了 Agent 专门引入 Rust;如果你是从零做一个高并发、资源受限、长生命周期的基础设施型 Agent 服务,Rust 值得考虑,尤其是需要单机扛大量 Agent 实例的场景。
4.6 运行服务:从同步阻塞到任务队列
最小骨架里可以直接用 FastAPI 暴露同步接口,但生产环境建议按第 3.5 节的决策点改成异步任务。这里给一个可以平滑迁移的中间方案:
# app.py from fastapi import FastAPI, BackgroundTasks from core.agent import AgentLoop app = FastAPI() agent = AgentLoop(llm, tools=TOOL_REGISTRY.values()) @app.post("/agent/run") async def run_agent(payload: dict, background_tasks: BackgroundTasks): """快速版:同步执行(适合短任务)""" result = await agent.run(payload["query"]) return {"result": result} @app.post("/agent/submit") async def submit_agent(payload: dict, background_tasks: BackgroundTasks): """稳妥版:后台任务 + 任务 ID 查询(适合长任务)""" task_id = f"task_{uuid4().hex[:8]}" background_tasks.add_task(execute_in_background, task_id, payload["query"]) return {"task_id": task_id}execute_in_background里需要做任务状态存储和并发信号量限流。信号量的上限就来自第 3.7 节说的供应商并发配额。
5. 从 Demo 到生产线的常见翻车现场:上下文、幻觉与反馈失灵
最后这节是实战中反复踩过的坑。前面是“怎么做”,这里是“别这么干”,全是血泪经验。
5.1 上下文漂移:Agent 干着干着就忘了目标任务
Demo 里跑两三轮工具调用,Agent 表现得像个明白人;到生产环境任务链路一长,它就开始创造新目标——用户让它“查天气”,它查完天气顺手开始推荐穿衣搭配,然后沿着这条线跑下去,越跑越偏。这不是聪明的体现,而是上下文漂移。
根因在于对话窗口被大量工具返回和中间推理填充后,原始目标的重要性被稀释了。工程对策有两个:一是每轮循环都把原始目标作为不可丢失的锚点,在 system prompt 里显式声明“你的根本目标是 X,所有操作必须服务于此目标”,并且每轮在输入里重新注入精简版任务描述;二是在 Agent 循环里加入目标校验:每完成一个工具调用,模型必须回答一句“当前状态是否仍与目标相关”,不相关就回到目标路径。我见过不少团队只用第一个对策,效果会衰减;两个一起用,长链路稳定性才有保障。
5.2 工具幻觉:模型会“脑补”成功,还带上下文爆炸
这是生产环境最可怕的坑。模型调用某个工具,工具实际返回了错误,但因为错误信息里包含了一些正常字段,模型就把整个结果当成成功,继续往深处跑。更隐蔽的一种是:工具返回空列表,模型在下一步推理里却把“没有数据”描述为“数据已处理完毕”,看起来像完成了任务,实际什么都没干。
对策在第 2.5 节已经埋了伏笔:结果必须带显式状态字段,且这个字段要被模型在推理时优先读取。我还会在 prompt 里加上一句:“任何工具返回结果必须基于success字段判断任务状态;即使工具返回了数据,也要先确认数据非空。”另外,如果任务结果最终依赖某个关键工具的返回,需要加一道独立校验器(规则代码)来验证“关键信息确实拿到了”,而不是完全信任模型的自我判断。
上下文爆炸则发生在工具返回结果不截断的时候。有人把 5 万字的网页正文直接塞给模型,然后下一轮模型输出的“理解结果”也要跟着塞进去,几轮下来窗口就满了。我的习惯是工具结果回填前必须经过“结构化、摘要化、截断化”三道处理,尽量把单次回填压到 2000 字符以内。
5.3 反馈死循环:反思机制成了无限内耗的出口
给 Agent 加上“反思”和“自我改进”之后,另一个坑会出现:遇到失败,模型不是换方案,而是反复总结“我上次失败了,这次我要更小心”,然后又用一样的路径冲一次,继续失败,继续总结。这是反馈回路设计得太“心理学”的结果——反思并没有改变行动。
对策是把反思从“自省”改成“定向重试策略”。不让模型泛泛地说“我要更小心”,而是要求它输出具体的策略变更:例如“上一次失败是参数缺少日期格式约束,这一次在调用前先补充格式转换步骤”。如果模型给出的新策略与旧策略没有本质区别(可以用文本相似度判断),就直接终止任务,避免死循环。另外,限制反思次数也很重要,我通常允许单任务内最多 2 次计划修正,超过就放弃并转人工。
5.4 没有评估集的回归测试,一切优化都可能是负优化
很多团队在 Agent 开发期跑得好好的,一改 prompt 或升级模型,就把一批原本成功的用例搞挂了。因为 Agent 的行为有随机性,没有回归集就无法判断“这次改进是真的进步,还是碰巧好运”。
我构建回归集的方法比较朴素但要花时间维护:从日志里挑出过去两周最有代表性的 50 到 100 条任务,标注每个任务的“预期关键行为”(比如必须调用某个工具、必须包含某个字段、最终演示必须不报错)。每次改动后跑一遍,比对行为的分布变化。这里的判定不一定用大模型打分——规则检查加关键步骤断言,在大多数场景下已经能拦住回归。
5.5 模型参数过拟合到单个供应商,迁移时半身不遂
最后提醒一个很多人不重视的问题:项目写到一定阶段,代码里到处是对某家模型输出格式的假设(比如message.tool_calls[0].function.name的解析方式)。一旦你要换供应商,改动波及面可能是全局的。
我最开始做 Agent 时也吃过这个亏。后来把模型层收敛到LLMClient.chat / complete / stream这三个接口,所有下游代码不直接依赖 SDK 类型,而是项目内自定义的Message / ToolCall数据结构。这样换模型时只改LLMClient内部,业务层完全不动。如果你的项目已经有大面积对 SDK 类型的直接引用了,尽早收敛——越晚迁移成本越高,这方面的改造早晚要做。
踩过这些坑之后,我的做法已经变得极其保守:任何 Agent 能力上线前,先跑回归集,再压一把上下文耗尽场景,最后用 trace 日志确认关键路径没有幻觉。这套流程虽然不性感,但至少不会让线上 Agent 变成大型翻车现场。如果你正准备把一个 Agent 原型推向生产,我的建议是先把第 3 节的七个决策点逐条过一遍,再对照第 4 节的骨架把代码补严——七个要素保证你不缺零件,七个决策点保证你不走弯路,剩下就是拿真实流量慢慢打磨了。