1. 从七个零件到七次抉择:AI Agent 到底怎么跑起来的
很多人第一次接触 AI Agent,脑子里浮现的画面大概是“一个会自己思考、自己动手的机器人”。但真到动手搭建的时候,你会发现它既不像科幻电影里那么玄乎,也不是简单调个 API 就能搞定的事。我前后用不同框架搭过十几个 Agent,从最早用 LangChain 拼工具链,到后来自己写循环调度、自己管上下文,踩过的坑基本能写一本小册子。这篇文章想干的事很直接:把 AI Agent 拆成七个必须想清楚的要素,再顺着它的运行链路,讲明白七个关键决策点分别卡在哪里、为什么这么选、选错了会出什么问题。
先把话说在前头,AI Agent 不是“更聪明的聊天机器人”。聊天机器人是你说一句它答一句,Agent 是你给它一个目标,它自己决定先干什么、再干什么、什么时候停下来。这个“自己决定”的过程,工程上就是循环机制加工具调用加上下文管理三件事拧在一起。LLM 是它的脑子,工具是它的手,上下文是它的记忆,循环是它的心跳。缺一个,这东西就跑不起来;任何一个设计得糙,它就会开始胡说八道、反复绕圈、或者干脆卡死。
这篇文章适合谁看?如果你已经调过 LLM 的 API,知道什么是 prompt、什么是 token,但一想到“让模型自己调工具、自己判断下一步”就有点发怵,那这篇就是写给你的。如果你已经在搭 Agent,但总觉得它不稳定、容易跑偏、上下文一长就犯傻,那这篇里的决策点拆解和排查思路应该能帮你省不少时间。我不打算堆术语,而是尽量用“为什么这么设计”的角度,把每个环节讲透。
2. 七要素拆解:一个能跑的 Agent 到底由什么组成
2.1 要素一:目标与任务描述——Agent 的“北极星”
任何 Agent 的起点都是一个目标。但“目标”这两个字在工程里非常容易含糊。你说“帮我整理一下这份数据”,模型根本不知道你要整理成什么格式、按什么维度、输出到哪里。我见过太多 Agent 跑偏的案例,根因不是模型笨,而是目标描述太模糊。
一个合格的目标描述至少要包含三样东西:做什么、做到什么程度、输出成什么形式。比如“读取 data.csv,按日期分组统计每日销售额,输出一个 Markdown 表格,保留两位小数”。这句话里,动作、粒度、格式全齐了。模型拿到这种描述,才有可能规划出正确的步骤。
这里有个经验:目标描述不要写成“帮我分析一下”,而要写成“执行 A,然后执行 B,最后输出 C”。Agent 不是人,它没有常识补全能力,你省掉的每一个字,它都可能用幻觉补上。
2.2 要素二:LLM 本体——脑子选不对,后面全白费
LLM 是 Agent 的推理核心。选模型这件事,很多人只看“哪个最强”,但实际工程里要看的是指令遵循能力、工具调用格式稳定性、上下文窗口大小、推理成本这四个维度的平衡。
指令遵循能力决定了它会不会老老实实按你给的格式输出。工具调用格式稳定性决定了它调工具时会不会把 JSON 写坏。上下文窗口决定了它能记住多少历史。推理成本决定了你跑一次任务要花多少钱。
我自己的选型习惯是:复杂规划用强模型,简单执行用轻模型。比如一个 Agent 里,负责“决定下一步做什么”的规划节点用能力强的模型,负责“把参数填进工具调用”的执行节点用便宜快的模型。这样整体成本能降下来,稳定性反而更好,因为执行节点的任务足够简单,轻模型反而不容易出错。
2.3 要素三:工具集——Agent 的手和脚
工具就是 Agent 能调用的外部能力。查数据库、发请求、读文件、算数学、调另一个模型,这些都是工具。工具设计的核心原则只有一条:接口要窄,描述要清,返回要稳。
接口窄的意思是,一个工具只干一件事。不要设计一个“万能工具”让它根据参数决定行为,那样模型很容易传错参数。描述要清的意思是,工具的说明文档要写清楚什么时候用、参数是什么格式、返回什么结构。返回要稳的意思是,工具的输出格式要固定,不要这次返回字符串、下次返回对象,模型会被搞晕。
我踩过的一个坑是:早期设计了一个工具,返回结果里既有成功信息又有错误信息,格式还不统一。结果模型经常把错误信息当成正常结果继续往下走,最后输出一堆垃圾。后来改成统一返回{status, data, error}结构,问题立刻少了一大半。
2.4 要素四:上下文与记忆——Agent 的工作台
上下文是 Agent 最容易出问题的地方。你可以把它理解成模型的工作台:台面上摆的东西越多,它越容易分心;摆的东西太少,它又缺信息。上下文管理的本质就是决定“每一步该让模型看到什么”。
这里要区分两种记忆:短期记忆和长期记忆。短期记忆是当前任务的对话历史,长期记忆是跨任务的知识沉淀。大部分 Agent 只需要短期记忆,但如果你要做“记住用户偏好”这种功能,就得引入长期记忆,通常用向量库或者结构化存储来做。
上下文长度是个硬约束。32k 上下文听起来不少,但如果你把工具返回的大段文本全塞进去,几轮就满了。我的做法是:工具返回结果先做摘要或截断,只保留关键字段;历史对话超过一定轮数就做压缩,把早期内容总结成一段话。这样能在有限窗口里塞进更多有效信息。
2.5 要素五:循环机制——Agent 的心跳
循环机制是 Agent 区别于普通 LLM 调用的核心。它的基本逻辑是:观察当前状态 → 决定下一步动作 → 执行动作 → 更新状态 → 判断是否结束。这个循环一直跑,直到任务完成或者触发终止条件。
循环设计里最关键的是终止条件。没有终止条件的 Agent 会无限跑下去,烧钱又烧时间。终止条件通常有三类:任务完成、达到最大步数、检测到无法继续。我一般会同时设最大步数和任务完成判断,双保险。
另一个关键是循环的“思考”环节。有些 Agent 每一步都让模型重新规划,这样灵活但慢;有些 Agent 先规划好整个步骤列表再逐步执行,这样快但不够灵活。实际用下来,混合模式比较稳:先让模型出一个大致计划,执行过程中允许它根据中间结果调整。
2.6 要素六:输出解析与格式化——把模型的话变成程序能懂的东西
模型输出的是自然语言,但程序需要的是结构化数据。输出解析就是干这个的。最常见的问题是模型不按格式输出,比如你要求 JSON,它给你一段带解释的文字。
解决办法有两个:一是用支持结构化输出的模型能力,很多模型现在有 JSON mode;二是在 prompt 里给明确的格式示例,并且加上“只输出 JSON,不要任何其他文字”这种强约束。我一般两个一起用,稳定性最高。
解析失败时要有兜底逻辑。不要直接崩掉,而是把原始输出记录下来,尝试用正则提取,或者让模型重新输出一次。这个兜底逻辑能救回不少偶发失败。
2.7 要素七:评估与可观测性——你怎么知道它干得好不好
Agent 跑起来之后,你得知道它每一步干了什么、为什么这么干、哪里出了问题。这就是可观测性。最基本的是日志:记录每一步的输入、输出、工具调用、耗时、token 消耗。
更进一步的是评估。你可以准备一批测试任务,定期跑一遍,看成功率、平均步数、平均成本。这样在改动 prompt 或工具之后,能快速判断是变好了还是变差了。
我自己的习惯是给每个 Agent 任务打一个 trace id,所有日志按 trace id 串起来。排查问题时,顺着 trace 一看就知道哪一步跑偏了。没有这个,排查基本靠猜。
3. 七个决策点:每一步都在做取舍
3.1 决策点一:规划方式选静态还是动态
静态规划是先把所有步骤列出来再执行,动态规划是每一步都重新决定下一步。静态的优点是可控、可预测、成本低;缺点是遇到意外情况不会调整。动态的优点是灵活;缺点是慢、贵、容易绕圈。
我的建议是:任务流程固定的场景用静态,比如“读文件→解析→写数据库”这种;任务需要探索的场景用动态,比如“帮我找一下这个 bug 的原因”。大部分生产环境其实是混合的:外层静态框架,内层动态决策。
3.2 决策点二:工具调用是并行还是串行
如果多个工具之间没有依赖关系,并行调用能省时间。但并行会带来状态管理复杂度和结果合并的问题。串行简单但慢。
判断标准很简单:后一个工具的参数是否依赖前一个工具的输出。依赖就串行,不依赖就可以考虑并行。我一般先串行跑通,确认逻辑没问题后再把无依赖的部分改成并行。
3.3 决策点三:上下文是全量保留还是压缩
全量保留简单,但很快会撑爆窗口。压缩能省空间,但会丢信息。我的做法是分层:最近几轮全量保留,更早的做摘要,工具返回的大文本只留关键字段。这样在窗口和信息的平衡上比较稳。
3.4 决策点四:错误处理是重试还是跳过
工具调用失败时,重试可能成功,也可能一直失败。跳过可能丢信息,也可能让流程继续。我的经验是:网络类错误重试,逻辑类错误跳过并记录。重试要设上限,一般两到三次,超过就放弃并让模型决定下一步。
3.5 决策点五:终止判断靠模型还是靠规则
靠模型判断灵活但可能误判,靠规则判断稳定但可能漏判。我一般两个都用:规则先做粗筛,比如步数超限、检测到完成关键词;模型做细判,比如“当前结果是否满足目标”。双保险比单靠一个稳。
3.6 决策点六:记忆存哪里、存多久
短期记忆放内存,任务结束就清。长期记忆放数据库或向量库,按需检索。存多久取决于业务需求,但一定要有清理机制,不然数据越堆越多,检索越来越慢。
3.7 决策点七:评估用人工还是自动
人工评估准但慢,自动评估快但可能不准。实际做法是:自动评估做日常监控,人工评估做定期抽查和关键节点确认。自动评估的指标可以是任务成功率、平均步数、工具调用成功率这些可量化的东西。
4. 实操:从零搭一个最小可用的 Agent
4.1 环境准备与依赖选择
先说环境。Python 生态里做 Agent 最顺手,依赖主要是 LLM SDK、HTTP 客户端、以及可选的向量库。如果你要用现成框架,LangChain、LlamaIndex 都可以,但我建议至少手写一遍最小循环,不然你永远不知道框架在背后干了什么。
pip install openai requests这是最小依赖。向量库按需加,比如chromadb或faiss。
4.2 定义工具:三个工具起步
先定义三个最基础的工具:读文件、写文件、执行计算。工具定义要包含名称、描述、参数 schema。
tools = [ { "name": "read_file", "description": "读取指定路径的文件内容", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "文件路径"} }, "required": ["path"] } }, { "name": "write_file", "description": "将内容写入指定路径的文件", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string"} }, "required": ["path", "content"] } }, { "name": "calculate", "description": "计算一个数学表达式的结果", "parameters": { "type": "object", "properties": { "expression": {"type": "string"} }, "required": ["expression"] } } ]注意每个工具的 description 都要写清楚“什么时候用”。模型是靠这个描述来决定调不调、调哪个的。
4.3 主循环实现:观察-决策-执行-更新
主循环是整个 Agent 的心脏。下面是一个简化版实现:
def run_agent(goal, max_steps=10): messages = [ {"role": "system", "content": "你是一个任务执行 Agent。根据目标决定下一步动作。"}, {"role": "user", "content": f"目标:{goal}"} ] for step in range(max_steps): response = call_llm(messages, tools=tools) if response.finish_reason == "stop": return response.content if response.tool_calls: for call in response.tool_calls: result = execute_tool(call.name, call.arguments) messages.append({ "role": "tool", "tool_call_id": call.id, "content": str(result) }) messages.append(response.message) return "达到最大步数,任务未完成"这个循环里,每一步都把历史消息发给模型,模型决定是调工具还是直接回答。调工具就把结果追加到消息里,继续下一轮。
4.4 上下文压缩:让窗口撑得更久
当消息历史变长时,需要压缩。最简单的做法是保留最近 N 轮,更早的做摘要:
def compress_messages(messages, keep_recent=6): if len(messages) <= keep_recent: return messages old = messages[:-keep_recent] recent = messages[-keep_recent:] summary = call_llm([ {"role": "user", "content": f"总结以下对话的关键信息:{old}"} ]) return [ {"role": "system", "content": f"历史摘要:{summary}"} ] + recent这个逻辑在每轮循环前跑一次,能有效控制上下文长度。
4.5 终止条件:三重保险
终止条件我一般设三个:模型主动说完成、达到最大步数、连续两次工具调用失败。三个里任意一个触发就停。
if response.finish_reason == "stop": return response.content if step >= max_steps: return "步数超限" if consecutive_failures >= 2: return "连续失败,终止"4.6 日志与追踪:每一步都留痕
日志是排查问题的命根子。每一步的输入、输出、工具调用、耗时都要记:
import logging logging.basicConfig(level=logging.INFO) def log_step(step, action, detail): logging.info(f"[step={step}] action={action} detail={detail}")生产环境建议用结构化日志,方便后续检索和分析。
5. 常见问题与排查技巧实录
5.1 模型不调工具,一直自己编答案
这是最常见的问题。原因通常是工具描述不够清楚,或者 prompt 里没有强调“必须用工具获取信息”。解决办法:在 system prompt 里明确写“不要凭记忆回答,需要信息时调用工具”,并且把工具描述写得更具体。
5.2 工具调用参数格式错误
模型有时候会把 JSON 写坏,比如多一个逗号、少一个引号。解决办法:用支持结构化输出的模型能力,或者在解析失败时让模型重新输出。我一般会加一个重试逻辑,失败就重新调一次,最多两次。
5.3 循环停不下来
通常是终止条件没设好,或者模型一直觉得“还没完成”。解决办法:设最大步数硬上限,并且在 prompt 里明确“如果信息足够就输出最终答案,不要继续调工具”。
5.4 上下文太长导致模型变傻
上下文一长,模型容易忽略中间的信息。解决办法:压缩历史、截断工具返回、把关键信息放在 prompt 开头或结尾。中间位置的信息最容易被忽略。
5.5 工具返回结果太大
工具返回一大段文本,直接塞进上下文会迅速撑爆窗口。解决办法:工具返回前先做摘要或只返回关键字段。比如读文件工具可以只返回前 2000 字符,或者让模型指定要读的部分。
5.6 排查速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 不调工具 | 工具描述不清 | 检查 description 和 system prompt |
| 参数错误 | 格式约束不足 | 加 JSON mode 或重试逻辑 |
| 停不下来 | 终止条件缺失 | 加最大步数和完成判断 |
| 上下文溢出 | 未压缩 | 加摘要和截断逻辑 |
| 结果不稳定 | 模型选型不当 | 换模型或拆分任务 |
5.7 几个我踩过的坑
第一个坑是工具描述写得太笼统。早期我写“查询数据”,模型根本不知道查什么、怎么查。后来改成“根据用户 ID 查询订单列表,返回订单号和金额”,调用准确率立刻上去了。
第二个坑是没设最大步数。有一次 Agent 陷入死循环,跑了上百步,烧了不少 token 才发现。后来所有 Agent 都强制设上限。
第三个坑是上下文没压缩。一个长任务跑到后面,模型开始重复之前的工作,因为早期信息被挤掉了。加了摘要逻辑后,这个问题基本消失。
6. 工程化落地:从能跑到好用还差什么
6.1 稳定性:重试、降级、熔断
生产环境的 Agent 必须考虑失败。工具调用失败要重试,模型调用失败要降级,连续失败要熔断。这些机制在 demo 里可以没有,但上线必须有。
6.2 成本控制:token 预算与模型分级
token 就是钱。每个任务设一个 token 预算,超了就停。模型分级用,简单任务用便宜模型,复杂任务用强模型。这样整体成本能降不少。
6.3 可观测性:日志、指标、追踪
日志记录每一步,指标监控成功率和耗时,追踪把一次任务的所有日志串起来。这三样齐了,排查问题才有依据。
6.4 评估体系:离线测试与在线监控
离线准备一批测试任务,每次改动跑一遍,看指标变化。在线监控真实任务的成功率和异常率。两个结合,才能持续优化。
6.5 安全边界:权限与沙箱
Agent 能调工具,就意味着它能对系统产生影响。文件读写、网络请求、数据库操作,都要有权限控制。敏感操作要加确认,或者放在沙箱里跑。这个不是可选项,是必选项。
我自己在实际操作中的体会是,Agent 的难点从来不在“让它跑起来”,而在“让它稳定地跑对”。七要素和七个决策点这套框架,最大的价值是让你在出问题时知道该往哪个方向查。工具不调用就看描述,循环停不下来就看终止条件,上下文出问题就看压缩逻辑。有了这套排查路径,大部分问题都能在几分钟内定位。最后再分享一个小技巧:每次改动只改一个变量,然后跑同一批测试任务对比结果。这样你才能知道到底是哪个改动起了作用,而不是靠感觉猜。