最近团队把一套 Agent 服务从 demo 推到线上,踩了不少坑,回头再看整个设计和实现,发现真正起决定性作用的,不是某个炫酷的框架,而是七个要素和七个决策点。所谓 AI Agent,简单说就是一个能感知环境、做规划、调工具、产生行动的智能体;而工程实现,就是把这四个动作变成可维护、可观测、可扩展的代码。这篇文章不打算讲某个特定产品的源码,而是把我拆解 Agent 工程实现时沉淀下来的思路、取舍和实操经验完整写出来。无论你是想基于 Rust 搞一个高性能运行时,还是用 Python 快速验证产品,这套方法应该都能用上。
1. 七要素:Agent 工程的最小骨架
很多刚接触 AI Agent 的同学容易陷入一个误区:看到 LangChain、AutoGen、CrewAI 这些框架,第一反应是“我该用哪个”,然后照着 demo 跑一遍,发现能对话、能调工具,就觉得已经搞定了。但真要放到生产环境,问题马上冒出来:任务一复杂就乱、上下文一长就超 token、工具一多就选错、跑着跑着就死循环。
我后来把问题收敛了一下,发现所有 Agent 系统,不管框架叫什么、用哪种语言写的,底层都逃不过这七个要素。它们是 Agent 工程的“最小骨架”,缺一个,系统就会有明显的短板。
1.1 为什么先聊“要素”而不是架构
市面上讲 Agent 架构的文章很多,有单 Agent、多 Agent、分层架构、事件驱动架构、图结构编排……但架构是“形态”,要素是“零件”。形态可以随便换,零件一个都不能少。先想清楚零件,再谈架构,才不会设计出一个看起来漂亮、跑起来崩的系统。
我习惯把这七个要素记成一句话:模型负责想,规划器负责拆,记忆负责记,工具负责干,执行器负责碰,状态负责串,护栏负责兜底。
1.2 七个要素逐个拆解
第一,模型引擎。这是 Agent 的“大脑”,不管你是接 OpenAI 的 API 还是本地部署开源模型,都需要把模型调用抽象成一个稳定的接口。工程上要注意的不只是“哪个模型聪明”,而是响应时间、token 成本、并发上限、多轮一致性和结构化输出能力。
第二,规划器。规划器决定“先做什么,后做什么”。有的 Agent 只有一层 ReAct 式的即时思考,有的会用思维树、任务图谱或者分层规划。规划器的复杂度直接决定 Agent 能处理的任务天花板。但它也是工程上最容易翻车的地方:规划太粗,任务完不成;规划太细,token 先爆了。
第三,记忆系统。记忆分短期和长期两类:短期记忆是当前任务里的上下文缓冲,长期记忆是需要持久化的知识、偏好、历史教训。工程实现里记忆不是“Redis 里存个字符串”那么简单,它涉及记忆怎么写、什么时候写、怎么检索、怎么压缩、怎么淘汰。
第四,工具层。工具是 Agent 跟外部世界交互的手脚,包括搜索引擎、数据库、代码执行器、内部 API 等。每个工具都要有清晰的描述、参数结构和调用协议。工程上最关键的是“工具描述写得好不好”——同一组工具,描述写清楚,模型调用准确率能高一大截。
第五,执行器。执行器负责真正调用工具、拿到结果、把结果反馈给模型。它像是 Agent 的“运动神经”,要做超时控制、重试、结果格式校验、异常捕获。很多 Agent 卡死不是模型蠢,而是执行器没有处理好“工具返回了一个非预期结果”这种情况。
第六,状态管理。Agent 是有状态的:当前进行到哪一步、已经调过哪些工具、收集了哪些信息、下一步能做什么。状态管理是工程实现里最容易被低估的要素。你可以在内存里用一个对象存状态,也可以把它落库、甚至用事件溯源;但不管怎么做,状态和上下文必须分清楚。
第七,评估与护栏。生产级 Agent 不能“裸奔”,要有输出合规检查、成本阈值、敏感操作二次确认、失败退出机制。我见过太多 Agent demo 能跑,但没人能回答“它答错了会怎样”“它调了危险工具会怎样”“预算超了会怎样”。护栏不是限制 Agent 能力,是给整个系统兜底。
七要素之间的关系可以用一张表格快速对照:
| 要素 | 对应问题 | 工程落地要点 |
|---|---|---|
| 模型引擎 | Agent 靠什么思考 | 模型接口抽象、输出解析、降级策略 |
| 规划器 | 先做什么后做什么 | 规划粒度控制、任务拆解策略 |
| 记忆系统 | 如何记住关键信息 | 短期缓冲、长期存储、检索与压缩 |
| 工具层 | 如何影响外部世界 | 工具 schema、描述质量、调用协议 |
| 执行器 | 如何安全地行动 | 超时、重试、错误处理、幂等 |
| 状态管理 | 当前进展在哪里 | 状态数据结构、持久化、恢复 |
| 评估与护栏 | 出错怎么办 | 校验规则、成本约束、审计日志 |
2. 七个决策点:从设计到上线必须做的关键取舍
七要素是理论骨架,真正落到代码里,你需要对“每个要素在你这套系统里怎么实现”做决策。我整理了七个高频决策点,这七个决策点几乎每个项目都会遇到,而且每个决策点都没有绝对正确的答案,只有适不适合你的场景。
2.1 决策点一:模型选型与调用协议
先想清楚用哪个模型,以及怎么调用。这不是“哪家强”的问题,而是你愿意拿什么换什么。闭源大模型效果稳定,但涉及敏感数据时会有额外限制;开源模型可以私有化部署,但需要自己处理推理性能和效果对齐。
工程实现上,我建议把模型调用封装成一个统一接口,不要直接散落在业务代码里调用具体 SDK。这样后续换模型、做灰度、加缓存都容易。调用协议上,优先考虑支持结构化输出或 function calling 的形式,而不是让模型自由发挥文本。结构化输出能大幅减少解析错误,这是我踩过最深的一个坑。
2.2 决策点二:任务拆解粒度
Agent 是把一个大目标拆成若干小步骤。拆多细,直接决定成功率、成本和延迟。如果每步都让模型自由思考,它可能走一步看一步,缺乏全局规划;如果一开始就拆成细粒度 DAG,又容易把模型锁死,遇到意外情况无法调整。
我的经验是“渐进式拆解”:第一层只拆阶段目标,比如“调研、整理、产出”;进入每个阶段后再拆具体动作。既保留灵活性,又不会让整个计划失控。任务越复杂,越要设计成可动态补充步骤的结构,而不是一次性把所有子任务都枚举出来。
2.3 决策点三:记忆的存与取
记忆决策点有两个维度:第一,哪些内容需要跨会话保存;第二,检索时用什么策略能快速找回相关内容。会话内的短期记忆通常靠拼接上下文完成,但超过模型窗口之后就必须压缩或摘要。
长期记忆我建议分成两层:一层是用户显式提供的偏好和事实,直接结构化存储;另一层是 Agent 运行过程中沉淀的经验,比如“上次用某个工具失败了,下次换个方式”,这类经验需要定期摘要并写入记忆库。检索方式不要一上来就搞高深的向量数据库,先用关键词加实体匹配往往就够用;等规模大了再上向量检索不迟。
2.4 决策点四:工具接口范式
工具怎么给模型用,是工程细节最多的地方。主流做法是 Function Calling:你给模型一些 JSON Schema,模型返回要调用的函数名和参数,你的代码去执行。另一种做法是让 Agent 生成代码或命令再执行,比如代码解释器模式。
我强烈建议能用 Function Calling 就用 Function Calling,因为它把“工具调用”限制在预定义范围内,好控制、好审计。如果工具特别多,比如超过二三十个,还要考虑给工具做分组和路由,避免模型在巨大的工具列表里迷失。工具描述字段一定要认真写,建议包含用途、适用场景、参数含义、常见报错。
2.5 决策点五:单 Agent 与多 Agent 拓扑
一个 Agent 包打天下,还是多个专业 Agent 协作?这个决策影响整个系统的复杂度和维护成本。单个 Agent 简单直接,但容易被上下文长度和任务复杂度卡住;多 Agent 可以分角色处理,比如一个做规划,一个写代码,一个做质检,但通信和组织成本会上升。
我的建议是:第一阶段先用单 Agent,把每个要素跑通;只有当单 Agent 在特定环节反复出错,或者需要不同模型各司其职时,再把那个环节替换成独立 Agent。不要为了多 Agent 而多 Agent。多 Agent 协作如果做不好,最后往往比单 Agent 更不稳定。
2.6 决策点六:会话与状态持久化
线上 Agent 必须处理“跑一半挂了怎么办”的问题。如果状态只保存在内存里,进程一重启,所有现场都没了。这就需要给状态设计持久化方案。一个比较实用的方式是:每完成一个关键步骤,就把状态快照写入数据库,记录当前进度、已收集的关键信息、待执行的下一步动作。
恢复场景可以做成两种:一种是失败后自动重试整个任务,这种情况要求所有工具调用都具备幂等性;另一种是从最近的快照继续执行,省去重复劳动。第二种体验更好,但实现成本更高。如果产品阶段还不确定,我建议先做“自动重试 + 幂等工具”,这是性价比最高的组合。
2.7 决策点七:安全、成本与可观测性
最后这个决策点最容易被忽略,但它决定你能不能在线上活下去。安全上要限制 Agent 能访问的资源范围,比如数据库只读账号、工具调用白名单、涉及敏感操作时的二次确认。成本上要给每次任务设置 token 和费用的上限,防止一个坏循环烧掉大量预算。
可观测性不是“打几条日志”就完了,要能把每一轮思考、每一次工具调用、每一个决策路径都能回放出来。也就是不仅要看结果,还要看过程。没有这个过程回放,Agent 出错时根本没法排查。
3. 工程实现:从零搭一个最小可跑 Agent
理论聊多了容易飘,直接看代码。下面这套实现非常朴素,但完整覆盖了七要素。我用 Python 写,因为表达逻辑清楚;如果你要用 Rust 重写,核心模式是一样的,只是把数据结构换成强类型,把并发换成异步任务。
3.1 技术栈选型与目录结构
只要你目标场景是刚起步、快速验证、团队熟悉 Python,优先选择 Python。Python 生态里模型 SDK、工具解析、数据处理都齐全。要是你的目标是高并发、低延迟、资源占用敏感,比如做一个文档级 Agent 服务,可以考虑用 Rust 做核心运行时,把 Agent 循环、状态管理、工具调度都放进去,然后通过 FFI 或 gRPC 给上层业务调用。
我建议的最小目录结构是这样的:
agent_service/ core/ # Agent 核心循环 memory/ # 短期与长期记忆 tools/ # 工具定义与执行 planner/ # 任务拆解策略 state/ # 状态持久化 guard/ # 评估与护栏 app.py # 服务入口3.2 核心数据结构
先定义模型输出和工具调用的数据结构。这里的关键是把 Agent 循环中的每一步都结构化,而不是让模型自由输出文本。
from typing import List, Dict, Any class AgentMessage: def __init__(self, role: str, content: str, tool_calls: List[Dict[str, Any]] = None): self.role = role self.content = content self.tool_calls = tool_calls or [] class ToolCall: def __init__(self, name: str, arguments: Dict[str, Any]): self.name = name self.arguments = arguments class StepResult: def __init__(self, thought: str, tool_calls: List[ToolCall], output: str): self.thought = thought self.tool_calls = tool_calls self.output = output这些结构看起来简单,但有一个很重要的作用:把模型返回的内容强制变成“可解析、可校验、可重放”的格式。后续无论是做日志回放还是状态恢复,都依赖这些结构化数据。
3.3 Agent 主循环实现
主循环是 Agent 的心脏。核心逻辑是:把当前任务、历史、工具定义拼成请求发给模型;模型决定是继续思考还是调用工具;如果调用工具就执行并返回结果;然后进入下一轮。
class AgentLoop: def __init__(self, model, tools, planner, memory, state_manager, guard): self.model = model self.tools = tools self.planner = planner self.memory = memory self.state_manager = state_manager self.guard = guard def run(self, task: str, session_id: str = "default") -> AgentMessage: state = self.state_manager.load(session_id) context = self.memory.build_context(task, state) for step in range(self.guard.max_steps): messages = context.to_messages() response = self.model.chat(messages) if not response.tool_calls: final_answer = response.content self.state_manager.save(session_id, state.finished(final_answer)) return final_answer for call in response.tool_calls: if not self.guard.check_tool_allowed(call.name): raise RuntimeError(f"tool not allowed: {call.name}") result = self.tools.execute(call.name, call.arguments) context.add_tool_result(call, result) self.state_manager.save(session_id, state.advance(step)) raise RuntimeError("max steps exceeded")这段代码强调了几个工程点:第一,最大步数必须有限制,否则 Agent 很容易死循环;第二,工具调用前做白名单校验;第三,每一步都保存状态,保证可恢复。实际项目里,这里还会加上 token 预算检查和错误重试逻辑。
3.4 工具定义与 Function Calling 示例
工具层最关键的是写好 JSON Schema。下面是一个“查询订单状态”的工具定义:
{ "name": "get_order_status", "description": "根据订单号查询当前订单状态,适用于电商场景", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,通常是字母开头的字符串" } }, "required": ["order_id"] } }这段描述里有几个容易被忽略的细节。一是描述里写了“适用于电商场景”,这能帮模型在多个工具里快速判断该不该选它。二是参数描述里补充了格式特征“字母开头的字符串”,这能减少模型乱传参数的概率。写工具描述的时候多花五分钟,调用准确率能提升不少。
3.5 状态持久化与恢复
生产环境我习惯把状态存到关系型数据库里,因为 Agent 任务通常有明确的“正在处理、成功、失败”等状态,用表结构存最直观。状态表大致长这样:
CREATE TABLE agent_state ( session_id TEXT PRIMARY KEY, task TEXT, current_step INT, status TEXT, snapshot JSONB, updated_at TIMESTAMP DEFAULT now() );每次关键步骤结束后,把 state 和收集到的关键信息写进 snapshot。恢复时读出 snapshot,重建上下文,从断点继续。这里要特别注意:恢复之后要重新给模型带上“我之前已经做过这些步骤”的摘要,否则模型可能会重复执行已经完成的操作。
3.6 部署与运行
部署上,我建议把 Agent 核心逻辑做成无状态服务,把状态和记忆放到外部存储中,这样才能水平扩容。任务入口可以放在消息队列里,一个任务一个消息,Worker 消费后执行 Agent 循环。这样避免同步 HTTP 调用时间太长导致超时。
配置方面,有几个值是必须显式配置的:模型名称、token 上限、最大步数、工具超时时间、成本上限。下面是一个最小配置示例:
model: name: "your-model-name" temperature: 0.2 max_tokens: 2000 agent: max_steps: 8 token_budget: 50000 cost_budget: 2.0 # 单任务成本上限(元) tool: timeout_ms: 5000 retry_times: 24. 常见问题与排查技巧实录
代码写完、服务上线,真正的战斗才刚开始。我整理了几个高频问题,每一个都在线上真实遇到过,并附上排查思路。
4.1 Token 超限与上下文管理
Agent 跑着跑着突然报 token 超限,这是最常见的问题。很多时候不是一段对话太长,而是工具返回结果太长。比如让模型调一个接口查询数据,接口返回了一万行 JSON,模型根本读不完。
解决思路有三个。第一,工具返回结果要做截断,只保留关键字段。第二,超过一定长度时要自动摘要。第三,清理历史对话,把早期轮次压缩成摘要。
我额外提醒一句,很多人不知道 token 到底怎么算的:它是模型处理文本的最小语义单元。中文通常一个字对应一到两个 token,英文一个单词可能拆成几个 token。工程上不要靠估算,要在每次调用前后上报 usage 数据,把 token 数据采集起来。
4.2 工具调用失败与重试
工具失败的原因很多:依赖服务超时、参数格式错误、权限不足等。Agent 的问题是,它可能因为一次工具失败就整个任务崩溃。正确的做法是给工具调用加两层保护:第一层是执行器层面的超时控制和自动重试;第二层是把错误信息重新喂给模型,让模型自行调整策略。
这里有两条经验,一是工具接口要做幂等设计,至少让重试不会产生副作用;二是错误信息要干净,不要让模型看到一大段带堆栈的技术日志,要转成一句人话,比如“订单号格式错误,请检查是否包含字母”。
4.3 死循环与目标漂移
Agent 卡在“不断调一个工具,然后失败,再调同一个工具”的循环里,这就是死循环。如果 Agent 每轮都在处理旁枝末节,忘记最初目标,这是目标漂移。最有效的护栏就是前面提到的 max_steps,但这只能兜底,不能真正防止循环。
我看到比较有效的方案是:每执行几步,就让模型做一个“与目标比对”的摘要,提醒它当前进度和目标的关系。这相当于给 Agent 加了一个自我校验的节点。加上这个节点后,循环出现的概率明显降低。
4.4 成本失控与限流
线上成本失控通常发生在两种场景:一是某个任务异常复杂,模型反复思考、反复调工具;二是用户高频调用,单次不贵但总量很大。前者靠单任务成本上限控制,后者要在接入层做限流,比如每个用户每分钟最多发起多少次任务。
我建议将成本监控做成实时仪表盘,按天、按用户、按任务类型三个维度统计 token 消耗。一旦看到异常波动,就能立刻定位到具体是哪类任务在烧钱。
4.5 排查技巧速查表
| 现象 | 优先排查点 | 常用手段 |
|---|---|---|
| 回答前后不一致 | 上下文是否被截断 | 检查 messages 实际拼接内容 |
| 不调用工具 | 工具描述是否清晰 | 改进 description 字段 |
| 调用错误工具 | 是否工具数量太多 | 工具分组或路由 |
| 任务卡住 | 工具超时或死循环 | 查看执行器日志与步数 |
| 费用突增 | 是否有异常长任务 | 看每次调用的 token 统计 |
| 恢复后重复执行 | 幂等性设计不到位 | 给工具调用加幂等键 |
| 输出格式乱 | 是否没有结构化解析 | 启用 function calling 或 JSON mode |
做 Agent 工程实现最反直觉的一点是:真正决定成败的往往不是“模型聪明不聪明”,而是工程细节有没有兜住“模型不够聪明”时的各种意外。我自己的体会是,先把最小骨架跑通,再逐个优化决策点,比一开始就套一个大而全的框架要靠谱得多。工具描述、状态快照、护栏设计这三样东西,我每次都会最先做好,因为它们能让你在后续迭代时少掉一半头发。