AI Agent 架构设计与简易落地
Table of content
AI Agent 架构设计与简易落地
引言
2024 到 2025 年,AI Agent 从一个概念迅速演变为工程实践的核心范式。OpenAI 的 Function Calling、Anthropic 的 MCP(Model Context Protocol)、Multi-Agent 编排框架——这些技术指向同一个核心问题:如何让大语言模型从文本生成器转变为能够调用外部工具、自主完成任务的执行体。
本文从最小可用 Agent(MVP)的实现经验出发,结合工程实践中的设计决策,系统梳理 Agent 的核心概念与架构原理,给出一份可直接落地的设计思路。
一、Agent 的本质:从 Chatbot 到自主执行体
要理解 Agent 的设计思路,首先要厘清 Agent 与传统 Chatbot 的本质区别。
1.1 能力演进阶梯
Agent 是 LLM 应用能力自然演进的结果,可划分为五个层级:
Level 0 Chatbot(纯对话)
输入 -> LLM -> 文本输出
无外部交互能力
Level 1 RAG Pipeline(检索增强)
回答前检索外部文档
知识不再局限于训练数据
Level 2 Tool-Calling Agent(工具调用)
LLM 可调外部函数/API 获取信息
单次判断 -> 单次调用 -> 单次回答
Level 3 Planning Agent / ReAct(规划推理)
多轮迭代:思考 -> 行动 -> 观察 -> 再思考
根据环境反馈动态调整策略
Level 4 Multi-Agent System(多智能体协同)
多个专业化 Agent 分工协作
存在协调者分配任务、共享记忆
为什么 Level 2 到 Level 3 是质变
Level 2 的 Tool-Calling 是一次性函数调用:模型判断需要某个工具,调用一次,拿到结果,回答用户。这只能处理简单查询(如查询当前时间),无法处理需要多步操作的问题。
例如,让 Agent 找出项目里所有用了过时代码的模块并给出修改建议。这需要读取目录结构、逐个分析文件、识别过时代码模式、生成修改建议。每一步的结果都影响下一步的决策。如果文件 A 里发现了过时的 API,Agent 需要知道已检查了 A,接下来检查 B;如果某个目录为空,Agent 需要跳过它。
Level 3 的核心突破是闭环反馈:Agent 运行一个持续的执行循环。每一轮工具执行的结果都会反馈给模型,模型基于这些新信息重新评估局势,决定下一步行动。这与下棋的过程类似:每走一步,观察对手的回应,再决定下一手,而不是开局就把所有棋步都算好。
1.2 Agent 的三个核心要素
大脑(LLM)
LLM 是 Agent 的推理中枢。LLM 的决策本质上是条件概率推理:给定历史上下文和当前观察,预测下一步最可能的内容。当 Agent 决定调用哪个工具时,可以将其理解为 LLM 在估计条件概率 P(调用 read_file | 用户问了文件相关问题,历史上下文),并生成概率最高的响应。
LLM 的决策质量高度依赖于上下文的完整性。如果历史记录中丢失了用户之前说过不要修改配置文件的信息,LLM 就可能做出错误的工具选择。
工具(Tools)
工具是 Agent 与外部世界交互的手段。没有工具,Agent 无法获取实时信息,无法操作文件系统,无法访问网络。工具让 Agent 具备了执行能力。
工具系统的设计遵循声明式与命令式的分离原则。每个工具对外暴露声明:Name(唯一标识)、Description(功能说明,供 LLM 决策使用)、Parameters(参数模式定义,JSON Schema)。工具的内部实现是命令式的:具体怎么执行、怎么处理异常。两者通过注册表解耦,新工具可以即插即用,无需修改 Agent 核心逻辑。
记忆(Memory)
记忆存储了 Agent 的上下文与历史经验。在 ReAct 循环中,记忆就是对话历史:用户输入、模型输出、工具返回结果,全部按时间顺序排列。模型每次做决策时都会读取这段历史。
记忆的容量受限于 LLM 的上下文窗口。当对话历史超过窗口容量时,早期信息会被截断。这引出了一个核心工程问题:如何在有限的记忆容量中保留最关键的信息。一种解决方案是上下文压缩(Context Compaction)——将早期对话历史摘要化,腾出空间给新内容。详见 6.2 节。
二、ReAct 架构:现代 Agent 的基石
2.1 ReAct 模式的核心循环
ReAct(Reasoning + Acting)由 Princeton 与 Google Research 于 2022 年提出,是 Agent 领域最主流的架构模式。核心是一个迭代的三阶段循环:
+----------------------------------------------------------------+
| ReAct Loop |
| |
| +----------+ +----------+ +----------+ |
| | Thought | ---->| Action | ---->| Observe | |
| | (思考) | | (行动) | | (观察) | |
| +-----|----+ +-----|----+ +-----|----+ |
| | | | |
| +-----------------+-----------------+ |
| | |
| (若无工具调用则输出最终答案) |
| | |
| v |
| +----------+ |
| | Final | |
| | Answer | |
| +----------+ |
+----------------------------------------------------------------+
Thought:LLM 分析当前状态,思考现在掌握了什么信息、距离目标还差什么、下一步该做什么。这个阶段是纯粹的内部推理,不涉及外部调用。
Action:基于 Thought 的结论,调用一个或多个工具。Action 是 Agent 与外部世界的唯一交互方式:读取文件、查询数据库、调用 API,都通过 Action 完成。
Observation:接收工具执行的结果。这个结果可能是成功返回的数据,也可能是错误信息。无论哪种情况,Observation 都是客观事实,会原样反馈给 LLM。
为什么 Observation 必须原样反馈
假设 Agent 调用 read_file 读取一个配置文件,工具返回了 5000 字的完整内容。如果中间层把内容摘要成 100 字再传给 LLM,那么当用户后续追问这个文件里数据库连接超时时间设置的是多少时,Agent 就答不上来了,因为它根本没有拿到完整信息。
Observation 的原样反馈保证了信息无损传递,这是闭环反馈有效性的前提。开发者可以在工具内部做格式化(如 JSON 转 Markdown),但语义内容必须保留完整。
2.2 多轮交互示例
用户提问:现在几点?顺便看看项目根目录下 Cargo.toml 的内容
================================================================
Round 1
================================================================
Agent 思考:用户问了两个问题:当前时间和文件内容。
我没有内置的时间和文件读取能力,需要调用工具。
先获取时间,再读取文件。
Agent 行动:调用 get_current_time()
工具返回:2026-06-08T10:30:00+08:00
================================================================
Round 2
================================================================
Agent 思考:时间已获取,接下来需要读取 Cargo.toml 的内容。
这是一个文件读取操作,调用 read_file 工具。
Agent 行动:调用 read_file(path="Cargo.toml")
工具返回:[package] name = "agent" version = "0.1.0" ...
================================================================
Round 3
================================================================
Agent 思考:两个问题所需的信息都已收集完毕。
不需要再调用工具,可以直接组织语言回答用户。
Agent 回答:现在是 2026 年 6 月 8 日上午 10 点 30 分。
Cargo.toml 的内容显示项目名为 agent,版本 0.1.0,...
关键机制:每一轮的 Observation 都会成为下一轮 Thought 的输入。没有 Round 1 的时间结果,Round 2 的 Thought 就不知道时间已经获取这个事实。这正是闭环反馈的价值:Agent 的行动改变了环境,环境的变化被感知,感知的结果指导下一步行动。
2.3 ReAct 的变体
ReAct(交错执行)
思考与行动一步一步交错推进。每调用一次工具,就停下来等结果,再根据结果决定下一步。最灵活:如果工具返回了意外结果(如文件不存在),Agent 可以立刻调整策略。代价是效率:需要多次 LLM 调用,每次都要等待模型生成响应。
对于交互式场景(如 CLI 聊天),ReAct 是最佳起点:简单、直观、每步状态都可见,调试友好。
ReWOO(先规划再并行)
先让 LLM 生成完整执行计划,然后并行执行所有工具调用,最后让 LLM 综合所有结果给出最终答案。适合多工具无依赖的场景。例如查询北京和上海的天气,两个查询互不依赖,可以并行发起,最后一起回答。ReWOO 只需要 1 次规划 + 1 次并行执行 + 1 次综合,而不是 2 次串行循环。
但 ReWOO 的弱点在于一次性规划:如果执行过程中发现某个工具返回了意外结果(如查询的 API 挂了),整个计划可能需要推翻重来。ReAct 在每一轮都可以根据最新观察调整策略,天然具备容错性。
Plan-and-Execute(瀑布式执行)
一次性生成完整计划,然后严格按顺序执行,中间不再让 LLM 参与决策。适合目标明确、步骤可预测的任务,如把 A 目录下的所有 .txt 文件复制到 B 目录。计划一旦生成,执行阶段就不需要再思考了。
缺点是缺乏灵活性。如果执行到一半发现 B 目录不存在,Plan-and-Execute 可能会直接失败,而 ReAct 可以灵活地先创建目录再继续。
Reflexion(自我纠错)
在 ReAct 的基础上增加评估环节:每次工具执行后,不仅把结果反馈给 LLM,还让 LLM 评估这一步执行得对不对、有没有更好的做法。如果发现做错了,Agent 会主动回退或重试。适合需要试错学习的场景,如写代码并通过测试。
| 模式 | 核心特点 | 适用场景 |
|---|---|---|
| ReAct | 逐步推进,每步都调整 | 交互式对话、调试友好 |
| ReWOO | 先规划再并行执行 | 多工具无依赖、追求效率 |
| Plan-and-Execute | 一次性计划,顺序执行 | 步骤可预测、目标明确 |
| Reflexion | 执行后自我评估纠错 | 试错学习、探索性任务 |
理解了 ReAct 及其变体后,我们来看看如何将这些模式工程化。
三、Agent 核心模块架构
3.1 模块关系图
+---------------------------+
| User Interface |
| (REPL / Web UI / API) |
+------------|--------------+
|
v
+----------------------------------------------------------------+
| Agent Loop |
| |
| +------------+ +------------+ +------------------+ |
| | 状态机 | | 消息历史 | | 工具调度器 | |
| +------|-----+ +------|-----+ +---------|--------+ |
| | | | |
| +-----------------+-----------------+ |
| | |
| v |
| +----------------+ |
| | LLM Client | |
| | (非流式/SSE) | |
| +--------|-------+ |
+---------------------------|------------------------------------+
|
v
+----------------------------------------------------------------+
| Supporting Services |
| |
| +----------+ +----------+ +----------+ +----------+ |
| |Tool System| | Sandbox | | Compactor| |Persistence| |
| | (注册表) | | (沙盒) | | (压缩器) | | (持久化) | |
| +----------+ +----------+ +----------+ +----------+ |
+----------------------------------------------------------------+
3.2 各模块职责
LLM Client
封装与外部 LLM API 的通信细节。不同厂商的 API 存在字段命名、认证方式、流式协议帧格式的差异。LLM Client 把这些差异屏蔽在内部,对外暴露统一的发送消息、接收响应接口。
职责包括:
- 协议适配:处理不同厂商 API 的字段差异。如某些模型支持 reasoning_content,某些不支持;某些用 tool_calls,某些用 function_call。
- 流式支持:SSE(Server-Sent Events)协议的解析与帧缓冲。流式响应推送多个 delta 片段,客户端需要把这些片段拼成完整消息。
- 错误分类:区分网络错误、认证错误、模型错误、上下文长度超限等不同异常类型,让上层做出不同应对。
Message
定义 Agent 与 LLM 之间通信的通用格式。OpenAI 的 Chat Completions 格式已成为事实标准。
核心字段:
- role:标识消息发送者身份。system 是系统指令,定义 Agent 的全局行为准则;user 是用户输入;assistant 是模型回复;tool 是工具执行结果的回填。
- content:消息的文本内容。
- tool_calls:assistant 决定调用工具时,携带工具调用的结构化描述:工具名和参数。
- tool_call_id:将工具结果回填给模型时必须携带的关联标识,用于把工具调用请求和对应的响应结果配对。假设 assistant 同时发起两个 read_file 调用(读 A 文件和读 B 文件),如果没有 tool_call_id,当两个工具都返回结果时,系统就不知道哪个结果对应哪个文件。
设计要点:所有扩展字段都必须是可选的。不同 LLM 厂商可能在标准格式之上增加自己的字段,使用可选字段可以保证前向兼容性:新字段出现不会导致旧解析器报错。
Tool System
负责工具的注册、描述生成和分发执行。每个工具对外暴露四个要素:
- Name:工具的唯一标识符。LLM 决定调用工具时输出 name 字段,系统根据这个名字找到对应的执行逻辑。
- Description:自然语言的功能说明。LLM 根据 Description 来判断这个工具能不能解决当前问题。Description 的质量直接决定了工具调用的准确率。
- Parameters:参数模式定义(JSON Schema)。告诉 LLM 这个工具需要什么输入:有哪些字段、字段类型是什么、哪些是必填的。
- Execute:实际的执行逻辑。接收 LLM 生成的参数,执行具体操作,返回结果。
声明式与命令式的分离让 Tool System 具备良好的扩展性。Name + Description + Parameters 是给 LLM 做决策用的声明;Execute 是给系统执行用的命令式实现。两者通过注册表解耦,新工具只需要注册自己的声明和执行体,开发者无需修改 Agent 核心逻辑。
Sandbox(沙盒)
Sandbox 是工具执行的运行时隔离环境。Agent 的工具系统让 LLM 能够执行任意操作,如果不对工具的权限加以限制,Agent 可能被诱导执行破坏性操作,如删除重要文件、读取敏感配置、执行恶意代码。
Sandbox 的核心目标是最小权限原则:工具只能访问它被允许访问的资源,只能执行它被允许执行的操作。具体实现分三层:
- 文件系统隔离:通过 chroot 或绑定挂载将工具限制在指定目录内。
- 系统调用过滤:通过 seccomp-bpf 限制工具可以使用的系统调用。
- 容器级隔离:将工具执行放入轻量级容器,提供完整的进程、网络、文件系统隔离。
沙盒的设计必须与工具注册表配合:每个工具在注册时声明自己的资源需求(需要访问哪些目录、是否需要网络),沙盒根据声明动态配置权限边界。
Agent Loop
整个系统的核心调度器。管理一个状态机,根据当前状态和输入事件决定下一步做什么。外层是一个迭代器:每轮用户交互可能包含多次 LLM 请求,每次工具调用后都要重新请求 LLM。
Compactor
在对话历史过长时进行自动压缩。当 LLM 的上下文窗口接近上限时,Compactor 把早期的对话历史压缩为一个摘要,腾出空间给新的对话。这是 Agent 能够进行长对话的关键能力。
Persistence
把对话历史保存到磁盘,实现跨会话的断点续聊。对话历史是一个事件序列:每次对话是一个事件,可以从事件序列重建完整状态。这与数据库的 WAL(Write-Ahead Log)和事件溯源(Event Sourcing)架构的哲学一致:不保存最终状态,而是保存导致状态变更的每个事件。
四、Agent Loop 状态机定义
4.1 为什么用状态机
Agent Loop 的行为可以很复杂:收到用户输入后要进入推理状态,推理过程中可能被用户中断,执行工具时可能出错,上下文太长时还需要压缩。如果没有明确的模型来管理这些状态和转移,代码复杂度会急剧上升,维护也将变得困难。
有限状态机(FSM)是可靠的建模工具。TCP 协议用它管理连接生命周期,编译器用它管理词法分析过程,游戏引擎用它管理角色动画状态。Agent Loop 同样适合用 FSM 建模:每个状态代表 Agent 在某一时刻的行为模式,状态之间的转移由外部事件触发。
状态机的好处是行为的可预测性。当系统处于某个状态时,能明确知道:当前在做什么、哪些事件可以触发状态转移、转移后进入什么状态、每个转移附带的副作用是什么。这让调试变得简单:当 Agent 做出奇怪行为时,看日志中状态转移的序列就能定位问题环节。
4.2 状态机模型
+-----------------+
| Idle |
| (等待输入) |
+--------|--------+
| 收到用户输入
v
+-----------------+
| Thinking |
| (LLM 推理中) |
+--------|--------+
| 响应接收完成
v
+------------------------+
| 响应是否包含工具调用? |
+------------|-----------+
|
+---------+---------+
| |
否 是
| |
v v
+-----------+ +-----------+
| Responding| | Executing |
| (直接回复)| | (执行工具)|
+-----|-----+ +-----|-----+
| |
| 工具结果返回
| v
| +-----------+
| | Observing |---------+
| |(回填结果) | 继续循环 |
| +-----------+ |
| |
v |
+-----------+ |
| Done |---------------------------+
| (完成) | 返回给用户,回到 Idle
+-----------+
4.3 状态详解
Idle —— 等待输入
Agent 的初始状态和默认状态。不做任何事情,等待用户的下一个输入。系统启动后、上一轮对话完成后、用户中断后,都会回到 Idle。
Thinking —— LLM 推理中
收到用户输入后,Agent 将用户消息追加到历史上下文中,向 LLM 发起请求。在响应完全返回之前,Agent 处于 Thinking 状态。
这个状态有一个关键设计问题:如果 Thinking 阶段耗时很长,用户能不能取消?答案是必须支持。就像数据库里可以杀掉慢查询,浏览器里可以停止加载卡住的页面。Agent 需要实现取消机制:用户按下 Ctrl+C 时,当前正在进行的 LLM 请求被优雅地终止,Agent 回到 Idle 状态。
实现上通常用取消令牌(Cancellation Token)模式:发起请求时创建一个令牌,用户中断时触发令牌,请求侧检测到触发后就停止等待并清理资源。
Executing —— 执行工具
LLM 返回的响应中包含工具调用请求时,Agent 进入 Executing 状态。按照工具调用的顺序逐个执行。如果有多个工具调用,可能串行执行(有依赖关系时)或并行执行(无依赖关系时)。
工具执行期间,Agent 不能向 LLM 发起新的请求。必须等待所有工具执行完毕,才能进入下一阶段。
Observing —— 回填观察结果
工具全部执行完毕后,Agent 把每个工具的返回结果封装成消息,追加到历史上下文中。这一步的本质是整理结果:确保所有执行结果都被正确记录,并且与对应的工具调用关联起来。
Observing 通常是一个很快的过程,主要做数据格式整理。完成后,Agent 自动回到 Thinking 状态,带着新的观察结果再次请求 LLM。
Responding —— 直接回复
LLM 的响应不包含任何工具调用时(即模型认为已经可以直接回答用户),Agent 从 Thinking 直接转移到 Responding。这是最短路径:用户问了一个纯知识性问题,Agent 不需要查任何外部信息,直接给出答案。
Done —— 本轮完成
当前用户轮次已经圆满完成。Agent 可以在这个时机做收尾工作:保存对话历史到磁盘(持久化)、打印本轮统计信息(token 消耗、耗时)。完成后回到 Idle,准备下一轮。
Error —— 发生错误
任何状态都可能因为错误进入 Error 状态。错误来源:网络故障导致 LLM 请求失败、工具执行异常(文件不存在、权限不足)、JSON 解析失败、上下文长度超限等。
Error 状态下的行为取决于错误类型:
- 网络抖动导致的临时失败:可以自动重试或回到 Idle 让用户重试。
- 工具执行失败(如文件不存在):错误信息被包装成 Observation 反馈给模型,让模型自行决定下一步。
- 严重错误(如 API 认证失败):可能需要终止程序并提示用户检查配置。
4.4 状态转移规则
| 当前状态 | 触发事件 | 下一状态 | 说明 |
|---|---|---|---|
| Idle | 用户输入 | Thinking | 用户消息加入历史,发起 LLM 请求 |
| Thinking | 响应完成,无工具调用 | Responding | 模型已给出最终答案 |
| Thinking | 响应完成,有工具调用 | Executing | 需要执行外部工具获取信息 |
| Thinking | 用户主动中断 | Idle | 丢弃本轮请求,回到等待状态 |
| Executing | 单个工具执行成功 | Executing | 继续执行剩余工具 |
| Executing | 所有工具执行完毕 | Observing | 收集全部结果,准备回填 |
| Executing | 工具执行失败 | Observing | 错误信息也要回填,让模型自行纠错 |
| Observing | 结果整理完成 | Thinking | 把工具结果加入历史,再次请求 LLM |
| Responding | 答案已返回 | Done | 本轮结束 |
| Done | 收尾完成 | Idle | 回到初始状态,等待下一轮 |
| 任意状态 | 不可恢复错误 | Error | 记录日志,视情况恢复或终止 |
4.5 轮次控制与终止条件
状态机定义了单轮交互内部的行为,Agent 还需要一个外层循环来控制最多迭代多少轮。
模型可能陷入反复调用工具但不收敛的状态:工具返回的结果一直不满足模型的期望,Agent 永远循环下去。这就是最大迭代次数(MAX_ITERATIONS)存在的意义:如果迭代次数超过阈值(通常设为 5-10 轮),强制终止当前轮次,向用户报告已尝试多次但未能收敛。
典型的轮次增长过程:
Iteration 0: 用户输入 -> Assistant 思考 -> 发现需要工具 -> 执行工具
Iteration 1: 工具结果 -> Assistant 思考 -> 发现还需要工具 -> 执行工具
Iteration 2: 工具结果 -> Assistant 思考 -> 信息已足够 -> 直接回答
-----------------------------------------------------------------
终止:返回最终答案给用户
每一轮的历史都在增长:用户消息 -> 助手响应(含工具调用)-> 工具结果 -> 助手再响应。这就像一个不断追加的日志文件。如果不加控制,会越来越长,最终导致上下文溢出。
五、简易落地:从零到可运行的 Agent
理论架构明确后,接下来进入实践环节。
5.1 实施顺序
落地实现建议按以下顺序推进。每个步骤都建立在前一步的基础上,逐步构建完整的 Agent:
Step 1: 消息模型 + LLM Client
目标:能与 LLM 进行最基本的对话
验证:输入问题,能得到文本回复
Step 2: 工具系统 + 注册表
目标:LLM 能根据 Description 选择合适的工具并生成参数
验证:输入"现在几点",能正确调用 get_current_time
Step 3: Agent Loop(状态机 + 迭代控制)
目标:实现 Thought -> Action -> Observation -> 再 Thought 的闭环
验证:输入"现在几点?再看看 Cargo.toml",能分两轮完成
Step 4: REPL + Trace 日志
目标:用户有地方输入,开发者能看到每一步的内部状态
验证:交互体验流畅,日志清晰可读
Step 5: 流式输出
目标:降低感知延迟,边生成边展示
验证:长回复时不用等完整生成
Step 6: 上下文压缩 + 持久化
目标:支持长对话,关闭后记忆不丢失
验证:聊 20 轮以上不崩溃,重启后能续聊
5.2 关键设计决策
消息契约的前向兼容性
不同 LLM 厂商可能在标准格式之上增加自己的字段(如 reasoning_content、annotations)。使用可选字段可以保证前向兼容性:新字段出现不会导致旧解析器报错。这与 HTTP 协议的 Header 设计哲学一致:遇到不认识的 Header,应该忽略而不是报错。
流式模式下的增量累积
LLM 推送的是碎片(delta),但工具调度的决策需要完整的消息。需要一个累加器:逐片接收 delta,当整个消息完整后,再一次性交给下游模块处理。
此外,推理模型的 thinking 内容(reasoning_content)不应该展示给用户,但必须原样回传给模型。部分模型要求请求消息中的 reasoning_content 与上一轮响应完全一致,否则将返回校验错误。
工具 Description 的质量
Description 不是越短越好。LLM 根据 Description 来判断是否调用工具。糟糕的 Description 如“读取文件“,LLM 不知道这个工具读什么、什么时候该用。好的 Description 如“读取指定路径的文本文件内容并返回,适用于获取配置文件、源代码、日志等文本内容,不支持读取二进制文件“。
MVP 阶段遵循从简单到复杂的原则。获取当前时间是最安全的工具:没有副作用,不会失败,适合作为第一个冒烟测试用例。
状态机的实现策略
极端 A:隐式状态机(布尔标志 + 嵌套条件)。代码简单,但随着复杂度增加会很快失控。
极端 B:显式状态机(状态枚举 + 转移函数)。实现复杂度高,但行为完全可预测。
推荐中间路线:隐式状态机起步,显式记录日志。代码层面用简单的循环和条件判断来实现,但在每个关键节点打印详细的 trace 日志:记录当前处于哪个阶段、做了什么决策、为什么转移。这让调试变得容易:当 Agent 做出奇怪决策时,可以从日志中清晰地看到推理过程。
REPL 的工程细节
- 行编辑:至少支持光标左右移动、删除、命令历史(上下箭头)。注意 Unicode 字符的处理:按一下退格应该删一个字符,而不是一个字节。处理不好会导致中文字符删除出现乱码。
- 特殊命令:退出程序(:quit)、手动触发上下文压缩(:compact)、查看当前状态(:status)等。
- 彩色日志:用户输入(绿色)、工具调用(黄色)、工具结果(青色)、错误(红色)、最终回答(白色)。
六、工程化增强:从玩具到生产
6.1 流式输出
流式输出能显著降低感知延迟。人眼的阅读速度大约是每分钟 300-500 个汉字,而 LLM 的生成速度通常可达每秒 10-50 个 token。如果采用非流式输出,用户需要等待 LLM 生成完整响应后才能看到任何内容。
流式输出的核心价值在于时间重叠:LLM 在生成后半段内容的同时,用户已经在阅读前半段了。总体的等待感被大幅削减。
流式实现中一个值得注意的技术细节是思考过程的实时展示。推理模型在输出最终答案前会先输出一段思考过程(reasoning_content)。在流式模式下,这段思考过程可以被实时展示出来,通常用灰色或斜体表示,让用户知道模型正在思考。这降低了用户的焦虑感:当屏幕上不断有内容更新时,用户知道系统在工作,而不是卡住了。
6.2 上下文压缩
多轮对话中,历史上下文会不断累积。每轮对话都在上一轮的基础上搭积木:用户消息、助手响应、工具结果,一层层堆叠。最终结果是 Prompt 越来越长,导致两个问题:
- 速度与成本:更长的 Prompt 意味着更长的处理时间和更高的 API 费用(按 token 计费)。
- 注意力涣散:LLM 的注意力机制并非完美,当上下文过长时,模型对早期内容的记忆会模糊,可能遗忘最初的任务指令。
压缩策略
上下文压缩的目标是用更短的文本保留等效的信息。这与很多经典系统设计的思路相通:操作系统在内存不足时会把不活跃的内存页换出到磁盘(swap);数据库会定期把冗长的事务日志压缩为一致的状态快照(checkpoint);日志系统会把旧日志压缩归档,只保留近期日志在线(log rotation)。
但上下文压缩有一个独特的约束:不能简单地截断(Truncation)。
在缓存系统中,LRU 策略假设最近访问过的数据更可能再次被使用,因此淘汰最久未访问的数据。但在对话中,早期的 System Prompt 和任务定义虽然很久没被直接访问,却往往是指导 Agent 行为的最关键信息。如果简单地淘汰最早的对话,Agent 可能会忘记自己的身份和任务目标。
因此,压缩策略必须遵循保头保尾压中间的原则:
原始对话历史
================================================================
[System] 系统指令(定义 Agent 身份和行为准则)
|
[User 1] 用户的第一个问题
|
[Assistant 1] 助手的响应(可能包含工具调用)
|
[Tool 1] 工具执行结果
|
... 中间多轮对话
|
[User N-1] 用户的倒数第二个问题
|
[Assistant N-1] 助手的响应
|
[User N] 用户的当前问题
压缩后的对话历史
================================================================
[System] 系统指令(保留,不可压缩)
|
[Summary] 摘要消息(压缩中间多轮对话的结果)
| 用户之前询问了项目结构,查看了时间和文件列表,
| 对 Cargo.toml 的内容感兴趣...
|
[User N-1] 最近一轮完整对话(保留,确保连贯性)
|
[Assistant N-1]
|
[User N] 用户的当前问题(保留)
边界规则
System 消息不可压缩:它定义了 Agent 的身份、行为准则和工具集合。修改 System Prompt 的后果不可预测。
保留最后完整轮次:从末尾倒序找到最后一个用户消息,把这个用户消息及其后的所有消息(助手响应、工具结果)完整保留。这确保了压缩后的上下文仍然是一个连贯的结尾:Agent 不会突然失去对最近几轮对话的记忆。
中间段交给 LLM 摘要:把 System 和保留段之间的所有对话交给另一个 LLM 调用,要求生成一段紧凑的摘要。摘要的 Prompt 设计很关键。根据实践经验,摘要应至少保留以下四类信息:关键事实、用户意图、已完成动作、待办事项。
压缩失败时的回退:如果摘要调用失败(网络故障、模型异常),不能让整个对话崩溃。回退策略是保留 System + 最后 1 轮,丢弃中间所有内容。这会丢失一些历史信息,但至少保证 Agent 还能继续工作。
触发时机
推荐组合策略:阈值触发为主,手动触发为辅。阈值可以设为上下文窗口的某个比例(如 50% 或 75%),给压缩操作本身留出足够的 token 空间。用户也可以通过特殊命令(如 :compact)手动触发压缩。
6.3 跨会话持久化
如果每次关闭 Agent 后重新打开,之前的对话都丢失了,那它就只能算是一个玩具。跨会话持久化让 Agent 具备了记忆连续性。
持久化设计的核心思路是事件日志机制。如果把每次对话看作一个事件,那么对话历史就是一个事件序列。不保存最终状态,而是保存导致状态变更的每个事件,任何时候都可以从事件序列重建状态。正如前文所述,这种设计与数据库 WAL 和事件溯源架构的理念一致。
持久化文件应该预留多会话的扩展空间。即使 MVP 阶段只支持单会话,数据格式也应该设计成可以容纳多个会话的集合。每个会话有独立的对话历史和元数据(如最后更新时间)。这为未来的会话切换功能预留了扩展点:用户可以在不同的项目上下文之间切换,每个上下文有独立的记忆。
自动保存的时机选择:每次对话轮次完成后自动保存是一个合理的策略。就像数据库每次事务提交后都写 WAL,确保崩溃后最多只丢失最后一笔未提交的数据。
6.4 错误处理与防护机制
死循环防护
如 4.5 节所述,模型可能陷入反复调用工具但不收敛的状态。除最大迭代次数(通常 5-10 轮)外,还需要以下防护机制:
工具异常处理
工具执行时可能失败(文件不存在、网络超时、权限不足)。关键在于不要让工具的错误导致整个 Agent 崩溃。相反,错误信息应该被包装成 Observation 反馈给 LLM。LLM 看到这个错误后,可能会尝试另一个路径(文件不存在?那我试试列出目录看看有什么),或者向用户解释问题(我无法访问那个文件,可能是因为权限不足),或者请求用户澄清(你想查看的是 Cargo.toml 还是 cargo.toml?)。
这种把错误交给模型处理的设计赋予了 Agent 自我修复的能力。这与传统编程中抛出异常、上层捕获的模式不同:Agent 没有严格的调用栈,它的异常处理就是基于上下文的重新推理。
上下文溢出防护
即使有了压缩机制,极端情况下上下文仍可能溢出(比如一次工具调用返回了 10 万字的日志文件)。此时需要两道防线:
工具结果截断:如果单个工具的返回结果超过某个字符上限(如 8000 字符),自动截断并附加提示(… 内容已截断,共 N 字符)。这类似于 Linux 命令的 head -n 100:只看前 100 行,知道个大概就够了。
紧急压缩:如果截断后上下文仍然超限,触发紧急压缩:只保留 System + 最后一轮,丢弃其他所有内容。这是降级策略,会丢失历史,但至少保证 Agent 不会彻底崩溃。
容器沙盒
Agent 的工具系统让 LLM 能够执行任意操作:读取文件、执行命令、访问网络。如果不对工具的权限加以限制,Agent 可能被诱导执行破坏性操作,如删除重要文件、读取敏感配置、执行恶意代码。容器沙盒是隔离工具执行环境的标准方案。
沙盒的核心目标是最小权限原则:工具只能访问它被允许访问的资源,只能执行它被允许执行的操作。具体实现可以分三层:
文件系统隔离:通过 chroot 或绑定挂载(bind mount)将工具限制在指定目录内。工具只能看到沙盒内的文件,无法访问宿主机的其他路径。这是 MVP 阶段最容易落地的方案。
系统调用过滤:通过 seccomp-bpf 或类似的机制,限制工具可以使用的系统调用。例如,允许 read/write/open,禁止 execve/socket/connect。这防止了工具执行任意程序或发起网络连接。
容器级隔离:将工具执行放入轻量级容器(如 Docker container 或 gVisor)。容器提供完整的进程、网络、文件系统隔离。这是生产环境推荐方案,但引入了额外的运行时依赖。
沙盒的落地顺序建议:MVP 阶段先用文件系统白名单(只允许访问指定目录),Phase 2 引入 chroot 隔离,Phase 3 引入系统调用过滤,生产环境再考虑完整容器隔离。沙盒的设计必须与工具注册表配合:每个工具在注册时声明自己的资源需求(需要访问哪些目录、是否需要网络),沙盒根据声明动态配置权限边界。
用户中断
用户可能在 Agent 思考的过程中改变主意(如发现问错了问题)。此时需要支持优雅的中断:取消当前的 LLM 请求,清理中间状态,然后回到 Idle 等待新的输入。
实现上通常使用取消令牌模式:发起请求时创建一个令牌,用户中断时触发令牌,请求侧检测到触发后就停止等待。这类似于 Go 语言的 context.WithCancel() 或 C# 的 CancellationTokenSource。
七、总结与展望
7.1 核心要点回顾
+----------------------------------------------------------------+
| Agent 全景图 |
| |
| +---------+ +---------+ +---------+ |
| | LLM | | Tools | | Memory | |
| | (大脑) | | (手脚) | | (记忆) | |
| +----|----+ +----|----+ +----|----+ |
| | | | |
| +----------------+----------------+ |
| | |
| v |
| +-----------------+ |
| | Agent Loop | |
| | | |
| | Thought -> | |
| | Action -> | |
| | Observation | |
| | (迭代直到完成) | |
| +-----------------+ |
| |
| 支撑系统:LLM Client / Compactor / Persistence / Sandbox |
+----------------------------------------------------------------+
核心定义:Agent 是一个运行在 LLM 之上的、具备闭环反馈能力的、有限状态机驱动的执行循环。
Agent 的三个核心要素是 LLM(推理中枢)、Tools(执行手段)、Memory(上下文记忆),由 Agent Loop 驱动循环。沙盒(Sandbox)不是与三者平行的核心要素,而是工具执行时的安全隔离层,属于支撑设施。它通过文件系统隔离、系统调用过滤或容器化手段,确保工具在最小权限范围内运行,防止 Agent 被诱导执行破坏性操作。
四个关键设计原则:
- ReAct 模式是 Agent 的核心循环:思考与行动的交替循环,每一轮的观察反馈指导下一轮的思考。它让 Agent 从一次性响应进化为持续推理。
- 状态机是 Agent Loop 的精确模型:明确的状态与转移让行为可预测、可调试。当 Agent 做出奇怪行为时,看状态转移日志就能定位问题。
- 消息契约是 Agent 与 LLM 之间的通用语言:OpenAI 兼容格式已成标准,其中的 tool_call_id 实现了请求与响应的可靠配对。
- 工具系统是 Agent 的扩展接口:声明式与命令式的分离让新工具可以即插即用,而不需要修改 Agent 的核心逻辑。
7.2 从 MVP 到生产的路径
Phase 1: MVP Agent(概念验证)
核心闭环:单轮 ReAct + 3 个基础工具 + REPL 交互
目标:验证 LLM + 工具 + 循环这个最小组合能否工作
Phase 2: 体验优化(交互升级)
流式输出 + 上下文压缩 + 用户中断 + 跨会话持久化
目标:让 Agent 从能用变成好用
Phase 3: 能力扩展(功能增强)
更多工具 + 文件编辑 + 代码执行 + 网络请求
目标:覆盖日常开发工作的主要场景
Phase 4: 多 Agent 协同(架构升级)
协调器 + 专业化 Agent + 共享记忆总线
目标:复杂任务的分解与并行执行
Phase 5: 自主编码 Agent(愿景)
需求理解 + 任务分解 + 自动规划 + 执行验证 + 迭代修复
目标:从辅助工具进化为协作伙伴
7.3 设计哲学
简单优先:先用最小集合跑通闭环,再逐步增强。能工作的简单系统胜过不能工作的复杂系统。
可见性:每个内部步骤都打印 trace 日志。调试 Agent 比调试普通程序更难,因为 LLM 的推理过程是黑盒。唯一增加可见性的手段是在关键节点记录日志,这既是调试工具,也是学习材料。
防御性:默认设置保守阈值(最大迭代次数、压缩阈值、工具结果截断长度)。这些阈值在异常时刻能防止系统失控。
模块化:LLM / Tools / Agent / Storage 解耦,可独立替换。今天用 Kimi,明天可以切换到 Claude;今天用本地文件工具,明天可以接入数据库。模块化的设计让系统具备演化能力。
参考与延伸阅读
- ReAct: Synergizing Reasoning and Acting in Language Models —— ReAct 模式的原始论文
- OpenAI Function Calling Guide —— 工具调用的协议规范
- Anthropic MCP (Model Context Protocol) —— 标准化的工具与上下文交互协议
- LangGraph: State Machine for Agents —— 用显式状态机编排 Agent 工作流
- AI Agent Development Complete Guide 2025 —— 2025 年 Agent 工程实践综述