1. 从七个零件到七次抉择:AI Agent 工程实现的底层逻辑
很多人第一次接触 AI Agent 这个概念,脑子里浮现的画面大概是科幻电影里那种能自己思考、自己行动的智能体。但真到了动手搭建的阶段,面对 LangChain、Dify、CrewAI 这些框架的文档,反而更迷糊了——到底什么才算 Agent?它和普通的 LLM 调用有什么区别?为什么我的 Agent 跑起来要么死循环,要么工具调不明白?
我刚开始做 Agent 开发的时候也踩过这些坑。后来慢慢摸索出一个认知:Agent 的本质不是某个框架,也不是某个模型,而是一套“感知-决策-执行”的循环机制。你把这个循环拆开看,它无非就是七个核心要素在支撑;你再把工程实现的过程拆开看,它无非就是七个关键决策点在等着你拍板。这篇文章我就按这个思路,把 Agent 从概念到落地的东西全部摊开讲一遍。
先说说这七个要素是什么。模型(Model)、指令(Instructions)、工具(Tools)、记忆(Memory)、编排(Orchestration)、循环(Loop)、输出解析(Output Parsing)。这七个东西缺一个,Agent 就跑不起来,或者跑起来也是个残废。很多人搭 Agent 失败,根本原因就是只关注了模型和工具,忽略了记忆和循环控制,结果做出来的东西要么没有上下文连续性,要么陷入无限调用工具的泥潭。
再来说七个决策点。当你真正开始写代码的时候,你会依次面临这些选择:用哪个模型做推理核心、指令怎么写才能让模型稳定输出、工具怎么定义和注册、记忆用什么方案存和取、编排逻辑用哪种范式、循环的终止条件怎么设、输出怎么解析和校验。每一个决策点都直接影响 Agent 的可靠性和成本。我见过太多项目在模型选型上纠结半天,结果在循环终止条件上随手写了个max_iterations=10就完事,最后线上跑起来 token 烧得飞快,问题还定位不到。
这篇文章适合谁看?如果你已经了解 LLM 的基本调用方式,想从“调 API”进阶到“搭系统”,那这篇内容就是为你准备的。如果你还在纠结用哪个框架,我也会在编排那一节把主流方案的取舍讲清楚。整篇内容我会尽量用大白话加实际代码片段来说明,不堆砌术语,重点放在“为什么这么选”和“踩过什么坑”上面。
2. 七要素拆解:一个 Agent 到底由什么构成
2.1 模型:推理核心的选型逻辑
模型是 Agent 的大脑,这个比喻虽然老套但确实准确。不过在实际工程中,模型选型远不是“哪个聪明用哪个”这么简单。你需要同时考虑推理能力、响应延迟、调用成本、上下文窗口大小、函数调用(Function Calling)的支持程度这几个维度。
我个人的经验是,Agent 场景下模型选型要分两层来看。第一层是主推理模型,负责理解用户意图、规划下一步动作、决定调用哪个工具。这一层对模型的指令遵循能力和逻辑推理能力要求最高。第二层是辅助模型,负责一些轻量级的任务,比如意图分类、实体抽取、结果格式化。这一层可以用更小更快的模型来降低成本。
以实际项目为例,我之前做一个客服工单自动分类和处理的 Agent,主推理用的是 Claude 3.5 Sonnet,辅助的意图识别用的是 GPT-4o-mini。为什么这么搭?因为主推理需要处理多轮对话中的复杂上下文,还要准确判断该调用哪个工具、传什么参数,Sonnet 在这方面的稳定性明显更好。而意图识别只是把用户消息分到十几个预设类别里,mini 模型完全够用,成本还低了一个数量级。
这里有个容易被忽略的点:Function Calling 的支持质量比模型本身的智商更重要。有些模型在通用 benchmark 上分数很高,但一到结构化输出和工具调用就拉胯,要么参数格式不对,要么该调工具的时候不调。你在选型的时候一定要拿自己的实际场景做测试,别只看排行榜。
提示:模型选型没有一劳永逸的答案。建议在项目初期用两到三个候选模型跑同一套测试用例,重点看工具调用的准确率和格式合规率,这两个指标比通用能力更能反映 Agent 场景下的真实表现。
2.2 指令:让模型稳定输出的关键
指令(Instructions)就是 System Prompt,但 Agent 场景下的指令比普通对话复杂得多。它不仅要告诉模型“你是谁、你要干什么”,还要明确“你能用什么工具、什么时候用、怎么用、不能用的时候怎么办”。
我见过很多 Agent 的指令写得像散文,洋洋洒洒几百字,结果模型该调工具的时候不调,不该调的时候乱调。问题出在哪?出在指令没有结构化。一个好的 Agent 指令应该包含这几个部分:
- 角色定义:一句话说清楚 Agent 的身份和职责边界
- 能力说明:列出可用的工具及其用途,每个工具一句话描述
- 决策规则:明确什么情况下调用什么工具,什么情况下直接回复
- 输出格式:规定最终回复的结构和风格
- 异常处理:工具调用失败、参数缺失、超出能力范围时怎么办
我习惯把指令写成类似“操作手册”的风格,而不是“角色扮演”的风格。举个例子,与其写“你是一个乐于助人的客服助手”,不如写“你负责处理用户关于订单的咨询。当用户询问订单状态时,调用 query_order 工具,参数为订单号。如果用户没有提供订单号,先询问订单号再调用工具。”
这种写法看起来有点机械,但实测下来模型的工具调用准确率能提升不少。原因很简单:模型在 Agent 场景下需要的是明确的决策路径,而不是模糊的角色设定。
2.3 工具:Agent 与外部世界交互的接口
工具(Tools)是 Agent 能力的延伸。没有工具,Agent 就只是一个会聊天的模型;有了工具,它才能查数据库、调 API、发邮件、操作文件。
工具的定义通常包含三个部分:名称、描述、参数 schema。名称要简洁明确,描述要说明这个工具做什么、什么时候用,参数 schema 要严格定义每个参数的类型、是否必填、取值范围。
这里有个坑我踩过好几次:工具描述写得太简略,导致模型不知道什么时候该用这个工具。比如你定义了一个search工具,描述只写“搜索信息”,模型就懵了——搜什么?什么时候搜?后来我把描述改成“根据关键词搜索产品知识库,适用于用户询问产品功能、价格、使用方法等场景”,调用准确率立刻上来了。
另一个坑是参数 schema 不够严格。比如一个日期参数,你只写了type: string,模型可能会传“明天”“下周三”这种自然语言,而不是2024-01-15这种标准格式。解决办法是在描述里明确格式要求,或者在工具实现层做一层参数清洗和转换。
# 工具定义示例(以 OpenAI Function Calling 格式为例) tools = [ { "type": "function", "function": { "name": "query_order_status", "description": "根据订单号查询订单的当前状态,包括待付款、已付款、已发货、已完成、已取消。适用于用户询问订单进度、物流状态的场景。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为纯数字,长度 12-18 位" } }, "required": ["order_id"] } } } ]2.4 记忆:让 Agent 不再“金鱼脑”
记忆(Memory)是 Agent 工程里最容易被低估的部分。很多人搭 Agent 的时候只关注单轮的工具调用,结果用户问完第一句,Agent 就忘了之前说过什么。这种体验在客服、助手类场景里是致命的。
Agent 的记忆通常分三层:短期记忆、长期记忆、工作记忆。短期记忆就是当前对话的上下文,一般直接放在 prompt 里。长期记忆是跨会话的信息,需要存到外部存储(比如向量数据库)里,用的时候检索出来。工作记忆是 Agent 在执行任务过程中产生的中间状态,比如已经调用了哪些工具、得到了什么结果。
我实际项目里的做法是:短期记忆用滑动窗口加摘要的方式管理,超过一定轮数就把早期对话压缩成摘要;长期记忆用向量库存用户画像和历史交互,检索时按相似度取 top-k;工作记忆用一个结构化的 state 对象来维护,每一步操作后更新。
注意:记忆不是越多越好。把大量无关的历史信息塞进 prompt,不仅浪费 token,还会干扰模型的判断。我一般会把短期记忆控制在 10 轮以内,超出部分做摘要压缩。
2.5 编排:Agent 的“交通指挥”
编排(Orchestration)决定了 Agent 的各个组件怎么协同工作。简单来说,就是“先做什么、后做什么、什么条件下走哪条路”。
常见的编排范式有几种:ReAct(推理-行动循环)、Plan-and-Execute(先规划再执行)、Multi-Agent(多 Agent 协作)。ReAct 适合简单的单步或少量步骤的任务,Plan-and-Execute 适合步骤多、需要提前规划的任务,Multi-Agent 适合角色分工明确的复杂场景。
选哪种编排方式,取决于你的任务复杂度和对可靠性的要求。我个人的经验是:能用 ReAct 解决的,就不要上 Multi-Agent。Multi-Agent 的调试成本是指数级上升的,两个 Agent 之间的通信一旦出问题,定位起来非常痛苦。
2.6 循环:Agent 的“心跳”
循环(Loop)是 Agent 区别于普通 LLM 调用的核心特征。普通调用是“输入-输出”一次完成,Agent 是“输入-推理-行动-观察-再推理”的循环,直到任务完成或触发终止条件。
循环机制里最关键的是终止条件。常见的终止条件有:模型输出了最终答案、达到了最大迭代次数、连续多次工具调用失败、超出了 token 预算。这几个条件通常要组合使用,缺一不可。
我见过最离谱的 bug 是一个 Agent 因为终止条件没写好,在工具调用失败后不断重试,一晚上烧了几百美元的 token。后来加了“连续三次工具调用失败就终止”的条件才解决。
2.7 输出解析:从模型输出到结构化数据
输出解析(Output Parsing)是把模型的自然语言输出转换成程序能用的结构化数据。在 Agent 场景下,输出解析主要处理两类内容:工具调用请求和最终回复。
工具调用请求通常由模型 API 直接返回结构化数据(比如 OpenAI 的tool_calls字段),不需要额外解析。但最终回复往往是自然语言,如果你需要程序进一步处理(比如存数据库、发通知),就需要做结构化解析。
我的做法是在指令里明确要求模型按 JSON 格式输出最终结果,然后用 Pydantic 或类似工具做校验。如果校验失败,就把错误信息返回给模型让它重新生成。这个重试机制能解决大部分格式问题。
3. 七个决策点:工程落地时的关键选择
3.1 决策点一:模型选型的权衡矩阵
前面讲了模型选型要考虑的维度,这里给一个更实操的决策框架。我一般会列一个矩阵,把候选模型按这几个维度打分:
| 维度 | 权重 | 说明 |
|---|---|---|
| 工具调用准确率 | 30% | 能否正确选择工具并传对参数 |
| 指令遵循能力 | 25% | 能否按指令要求格式输出 |
| 推理能力 | 20% | 多步推理和规划的质量 |
| 响应延迟 | 15% | 首 token 时间和总响应时间 |
| 成本 | 10% | 每百万 token 的输入输出价格 |
权重可以根据你的场景调整。比如实时对话场景延迟权重调高,批处理场景成本权重调高。打分的时候一定要用你自己的测试用例,别用公开 benchmark。
3.2 决策点二:指令的粒度控制
指令写多细?这是一个很微妙的平衡。写太粗,模型自由发挥,行为不可控;写太细,指令变成了一本操作手册,维护成本高,而且模型可能因为指令太长而忽略部分内容。
我的经验法则是:核心决策路径写细,边缘情况写粗。比如“用户问订单状态就调 query_order”这种主流程要写清楚,但“用户骂人怎么办”这种边缘情况可以只写一句“保持礼貌,不回应攻击性内容”。
另外,指令要定期 review。我一般每两周会把线上 Agent 的失败案例捞出来看一遍,如果发现某类问题反复出现,就在指令里补一条规则。
3.3 决策点三:工具的数量与粒度
工具是不是越多越好?不是。工具太多会导致两个问题:一是模型选择困难,调用准确率下降;二是 prompt 里工具描述占用的 token 变多,成本和延迟都上升。
我一般会把工具数量控制在 10 个以内。如果业务需要更多能力,我会做一层抽象,把相似的工具合并,或者用“工具路由”的方式先分类再选择。
工具的粒度也很重要。太粗,一个工具干太多事,参数复杂,模型容易传错;太细,工具数量爆炸。我倾向于一个工具做一件事,参数控制在 3 个以内。
3.4 决策点四:记忆方案的选型
记忆方案的选择取决于你的场景需求。如果是单次会话的助手,短期记忆加滑动窗口就够了。如果需要跨会话记住用户偏好,就得上向量数据库。
向量数据库的选型也有讲究。小规模场景(百万级以下)用 Chroma 或 FAISS 就够了,部署简单。大规模场景可以考虑 Milvus 或 Qdrant。如果已经在用云服务,直接用云厂商的向量检索服务也行。
提示:记忆检索的 top-k 不要设太大,一般 3-5 条就够了。检索出来的内容要加上相关性分数过滤,低于阈值的直接丢弃,避免引入噪声。
3.5 决策点五:编排范式的选择
ReAct、Plan-and-Execute、Multi-Agent 这三种范式怎么选?我给一个简单的判断标准:
- 任务步骤少于 5 步,且步骤之间依赖关系简单:用 ReAct
- 任务步骤多于 5 步,或需要提前规划资源:用 Plan-and-Execute
- 任务需要多个专业角色协作,且角色间有明确分工:用 Multi-Agent
大部分场景 ReAct 就够了。Plan-and-Execute 适合那种“先出方案再执行”的场景,比如旅行规划、项目排期。Multi-Agent 适合模拟团队协作的场景,比如软件开发、内容生产流水线。
3.6 决策点六:循环终止条件的设置
终止条件是 Agent 可靠性的最后一道防线。我一般会设置这几层保护:
- 最大迭代次数:硬性上限,一般设 10-15 次
- 连续失败次数:连续 3 次工具调用失败就终止
- token 预算:单次会话总 token 超过阈值就终止
- 超时时间:单次会话总时长超过阈值就终止
这几层保护要同时生效,任何一层触发都终止循环并返回当前状态。千万别只设一个最大迭代次数就完事。
3.7 决策点七:输出解析的容错设计
输出解析的容错设计经常被忽略。模型输出格式不对是常态,不是异常。你的解析逻辑必须能处理这些情况:
- JSON 格式错误:尝试修复(比如补全括号、去掉多余逗号)
- 字段缺失:用默认值填充或触发重试
- 类型不匹配:尝试转换或触发重试
- 内容不合规:过滤或触发重试
我一般会做两层解析:第一层用宽松的模式提取关键信息,第二层用严格的 schema 校验。第一层保证能提取到东西,第二层保证提取到的东西能用。
4. 实操过程:从零搭建一个可用的 Agent
4.1 环境准备与依赖安装
我以 Python 技术栈为例,演示一个最小可用的 Agent 搭建过程。需要安装的依赖包括:
pip install openai pydantic chromadb如果你用 LangChain 或 LlamaIndex,也可以直接装对应的包。但我建议初期先用原生 SDK 手写一遍,理解每个环节在做什么,后面再用框架提效。
4.2 定义工具与工具注册
先定义两个简单的工具:一个查天气,一个算数学。
import json from pydantic import BaseModel, Field class WeatherInput(BaseModel): city: str = Field(description="城市名称,如北京、上海") class CalculatorInput(BaseModel): expression: str = Field(description="数学表达式,如 2+3*4") def get_weather(city: str) -> str: # 模拟天气查询 weather_data = {"北京": "晴,25°C", "上海": "多云,28°C"} return weather_data.get(city, f"未找到{city}的天气信息") def calculate(expression: str) -> str: try: result = eval(expression) return str(result) except Exception as e: return f"计算错误:{e}" tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气", "parameters": WeatherInput.model_json_schema() } }, { "type": "function", "function": { "name": "calculate", "description": "计算数学表达式的结果", "parameters": CalculatorInput.model_json_schema() } } ] tool_map = { "get_weather": get_weather, "calculate": calculate }4.3 构建 Agent 循环
核心循环逻辑如下:
from openai import OpenAI client = OpenAI() def run_agent(user_input: str, max_iterations: int = 10): messages = [ {"role": "system", "content": "你是一个助手,可以查天气和做计算。根据用户需求调用合适的工具。"}, {"role": "user", "content": user_input} ] for i in range(max_iterations): response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=tools, tool_choice="auto" ) message = response.choices[0].message # 如果没有工具调用,说明模型给出了最终答案 if not message.tool_calls: return message.content # 处理工具调用 messages.append(message) for tool_call in message.tool_calls: func_name = tool_call.function.name func_args = json.loads(tool_call.function.arguments) if func_name in tool_map: result = tool_map[func_name](**func_args) else: result = f"未知工具:{func_name}" messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": str(result) }) return "达到最大迭代次数,任务未完成"这段代码就是一个最基础的 ReAct 循环。你可以直接跑起来试试,输入“北京天气怎么样”或者“帮我算一下 123 乘以 456”。
4.4 加入记忆与状态管理
上面的版本没有记忆,每次调用都是独立的。要加入多轮对话记忆,只需要把 messages 维护在会话级别:
class AgentSession: def __init__(self): self.messages = [ {"role": "system", "content": "你是一个助手,可以查天气和做计算。"} ] def chat(self, user_input: str): self.messages.append({"role": "user", "content": user_input}) # ... 循环逻辑同上,但操作 self.messages # 循环结束后把最终回复也 append 进去这样同一个 session 里的多轮对话就能共享上下文了。
4.5 参数计算与成本估算
Agent 的成本主要来自 token 消耗。一次工具调用的循环通常包含:一次输入(含系统指令、历史消息、工具定义)、一次输出(含工具调用请求)、一次工具结果输入、一次最终输出。粗略估算,一次完整的工具调用循环消耗的 token 是普通对话的 3-5 倍。
以 GPT-4o-mini 为例,输入 $0.15/百万 token,输出 $0.6/百万 token。假设一次循环输入 2000 token、输出 500 token,成本大约是 $0.0003 + $0.0003 = $0.0006。一天 1000 次调用就是 $0.6。看起来不多,但如果你的 Agent 经常陷入循环或者工具描述很长,成本会快速上升。
注意:工具定义本身也占 token。工具越多、描述越长,每次调用的输入 token 就越多。我一般会把工具描述控制在 50 字以内,工具数量控制在 10 个以内。
5. 常见问题与排查技巧实录
5.1 工具调用不触发或触发错误
这是最常见的问题。表现是模型该调工具的时候不调,或者调了错误的工具。排查思路:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全不调工具 | 指令没说明工具用途 | 在指令里明确工具的使用场景 |
| 调错工具 | 工具描述太相似 | 区分工具描述,突出差异 |
| 参数传错 | schema 不严格 | 加参数描述和格式约束 |
| 该调不调 | 模型能力不足 | 换工具调用能力更强的模型 |
我遇到过一个案例:两个工具分别是“查询订单”和“查询物流”,描述都写得很泛,模型经常搞混。后来把描述改成“查询订单的支付状态和金额”和“查询订单的物流轨迹和预计送达时间”,准确率立刻上去了。
5.2 循环无法终止
循环终止不了通常是因为终止条件没设好。检查这几项:
- 最大迭代次数是否设置
- 工具调用失败是否有重试上限
- 模型是否可能一直输出工具调用而不给最终答案
我一般会在指令里加一句“当你已经获取到足够信息时,直接给出最终答案,不要再调用工具”。这句话能减少不少无效循环。
5.3 输出格式不符合预期
模型输出格式不对,优先检查指令里的格式要求是否明确。如果指令里写了“按 JSON 输出”但模型还是输出自然语言,可以试试:
- 在指令里给一个输出示例
- 用 response_format 参数强制 JSON 模式(如果模型支持)
- 加一层解析容错,解析失败时让模型重新生成
5.4 记忆检索不相关
向量检索出来的记忆和当前问题不相关,通常是 embedding 模型选得不好或者 top-k 设得太大。解决办法:
- 换一个更适合你领域的 embedding 模型
- 减小 top-k,加相似度阈值过滤
- 在检索前先做 query 改写,让检索词更精准
5.5 多轮对话中上下文丢失
多轮对话中 Agent 忘了之前说过的话,检查 messages 是否正确维护。常见错误是每次调用都重新初始化 messages,导致历史丢失。另外,如果做了上下文压缩,检查摘要是否保留了关键信息。
6. 进阶方向:从能用走向好用
6.1 引入评估体系
Agent 搭起来只是第一步,能不能稳定运行才是关键。我建议从第一天就建立评估体系,至少包括:
- 工具调用准确率:正确调用工具的比例
- 任务完成率:成功完成用户请求的比例
- 平均循环次数:完成任务平均需要几轮
- 平均 token 消耗:单次任务的平均成本
这些指标定期统计,能帮你发现性能退化。
6.2 加入人工兜底
再好的 Agent 也有搞不定的时候。在关键业务场景里,一定要有转人工的机制。我的做法是:当 Agent 连续失败、或者用户明确要求转人工、或者任务超出 Agent 能力范围时,自动创建工单转给人工处理。
6.3 持续优化指令与工具
Agent 的优化是一个持续过程。我一般每周会做一次失败案例分析,把典型问题归类,然后针对性地优化指令或工具描述。这个习惯坚持下来,Agent 的成功率会稳步提升。
6.4 安全与合规考量
Agent 能调工具就意味着它能产生实际影响。在工具层面一定要做权限控制,敏感操作(比如删除数据、发送消息)要加确认机制。另外,工具返回的内容要做过滤,避免注入攻击。
提示:工具的执行结果不要直接拼接到 prompt 里,先做一层清洗和截断。恶意内容可能通过工具返回值注入到 Agent 的上下文中。
7. 我踩过的那些坑
说几个印象深刻的踩坑经历。第一个是早期做的一个日程管理 Agent,工具里有一个“创建日程”的功能。测试的时候一切正常,上线后用户说“帮我安排明天下午三点开会”,Agent 创建了一个标题为“明天下午三点开会”的日程,时间是空的。问题出在工具参数设计上——我把时间和标题合成了一个参数,模型分不清哪个是时间哪个是标题。后来拆成两个参数就好了。
第二个坑是循环控制。有个 Agent 在工具调用失败后会不断重试同一个工具,因为我在代码里写了“如果工具调用失败,把错误信息返回给模型让它重试”,但没设重试上限。结果模型看到错误信息后换了个参数继续调,一直调不通就一直调。后来加了连续失败三次就终止的逻辑。
第三个坑是记忆污染。有个场景是把用户的历史对话全部存进向量库,检索的时候按相似度取 top-5。结果有一次用户问了一个和三个月前完全无关的问题,检索出来的全是三个月前的旧对话,Agent 的回答完全跑偏。后来加了时间衰减因子,越新的记忆权重越高,问题才解决。
这些坑说到底都是工程细节问题,但每一个都能让 Agent 从“能用”变成“不能用”。Agent 开发就是这样,概念不难,难的是把每个细节都考虑到。