很多人问我说,现在现成的 Agent 框架那么多,为什么还要自己搭一套?我的回答是:框架解决的是“通用问题”,而你的业务解决的是“特定问题”。这中间的空隙,就是你要不要自建 Agent 框架的真正分界线。这一章我不会去讲某个具体框架的 API 怎么调,而是把“第七章 构建你的 Agent 框架”最核心的部分——7.1 框架整体架构设计——掰开揉碎,讲清楚一个能支撑复杂业务、能演进、能维护的 Agent 框架,它的架构到底应该怎么搭。
这篇文章的定位是给已经跑通过几个 Demo、但一上生产就开始凌乱的团队和个人开发者看的。你不需要是资深系统架构师,但你需要对 Python 比较熟,最好已经写过几个基于大模型的应用。我会从框架的职责边界、核心抽象、编排机制、上下文管理、工程化落地这几个维度往下拆,并且在关键设计决策上告诉你“为什么这样做”,而不是只丢给你一张架构图。
1. 自建 Agent 框架前,先想清楚框架的职责边界
很多人一说“自建 Agent 框架”,第一反应就是“我们要写一个大而全的框架,什么都要有”。这个思路基本上一开始就输了。真正能落地、能被团队长期维护的框架,通常只做窄而深的事情:把大模型调用、工具调用、状态流转、上下文管理这几件事抽象好,然后剩下的全部交给业务层。
1.1 现成框架很香,但定制需求迟早会顶到天花板
市面上有不少成熟的 Agent 框架,它们解决了统一接口、内置工具链、插件生态等通用问题。但你一旦进入真实业务,会遇到几个特别扎心的情况:
- 内置的 Agent 循环逻辑是黑盒,你无法精确控制每一轮决策的触发条件;
- 工具调用的错误处理策略是通用的,无法适配你业务的幂等性要求;
- 上下文管理机制偏通用,遇到行业领域知识、长流程任务,内存和 token 消耗双双失控;
- 团队定制了 Prompts 体系后,框架升级一个版本,行为就变了,双休日加班做回归。
这些问题的本质,不是现成框架“做错了”,而是“通用”和“定制”之间天生有张力。自建框架的意义,不是什么都自己写,而是把框架的底盘——也就是 Agent 的定义方式、执行循环、上下文管线和工具协议——握在自己手里。
1.2 明确边界:这个框架不做什么,反而更重要
我在设计自己的 Agent 框架时,第一件事不是画架构图,而是列了一份“框架不做什么”的清单:
| 框架职责(做) | 框架不职责(不做) |
|---|---|
| Agent 的抽象定义与生命周期管理 | 业务具体流程编排逻辑 |
| 大模型 Provider 的统一接入 | 具体 Prompt 内容的编写 |
| 工具调用的协议与注册机制 | 业务工具的具体实现 |
| 上下文的分层管理与压缩策略 | 存储基础设施选型(如用什么数据库) |
| 多 Agent 协作的编排原语 | 具体任务的路线规划 |
| 可观测性埋点与日志规范 | 具体的监控大盘展示 |
| 错误处理与重试策略的通用规则 | 业务重试语义(如支付场景的特殊处理) |
这份清单最核心的价值,是防止框架侵入业务。我见过太多团队把框架越写越厚,最后框架里塞满了业务判断,业务代码里也到处是框架钩子,两边的耦合到谁都动不了的地步。
提示:自建框架的第一行代码,建议从“Agent 抽象规范文档”开始写,而不是从一个基类开始写。
2. 核心抽象层:Agent、LLM、工具与记忆的职责划分
框架的底层,是我最先稳定的四个核心抽象:Agent、LLM Provider、Tool 和 Memory。这四个概念听起来简单,但真正设计好了并不容易,因为它们之间的交互方式,决定了整个框架的复杂度和扩展性。
2.1 Agent 对象不是“一个类”,而是一份契约
我刚做第一版框架时,把 Agent 设计成了一个充满业务方法的大类,结果每来一个新场景,就要继承和重写。后来我重构了设计:Agent 本质上是一份“契约”——一个对象只要满足以下条件,框架就可以驱动它:
- 有一个系统提示词模板(System Prompt),可以接收上下文变量动态渲染;
- 有一个工具注册表,标明它自己能调用哪些工具;
- 有明确的输入输出模型(Input/Output Schema);
- 有一个终止条件定义,即什么时候该结束本轮执行。
这里我用 Python 的 Protocol 做一个简单示例,这个设计思路不受语言限制:
from typing import Protocol, List, Optional from pydantic import BaseModel class AgentInput(BaseModel): user_message: str session_state: dict extra_vars: dict = {} class AgentOutput(BaseModel): reply: str handoff_to: Optional[str] = None state_updates: dict = {} class ToolSpec(BaseModel): name: str description: str parameters_schema: dict class IAgent(Protocol): name: str system_prompt_template: str tools: List[ToolSpec] async def build_messages( self, agent_input: AgentInput, memory: "MemoryManager" ) -> List[dict]: ... async def parse_response(self, response: dict) -> AgentOutput: ...为什么我要把 Agent 设计成协议而不是基类?因为在真实业务里,有的 Agent 可能不需要走大模型,它就是一条规则路由;有的 Agent 需要调用外部 gRPC 服务;有的 Agent 后续会替换成微调过的自研小模型。协议的方式允许这些异构实现“无痛接入”。
这套设计的关键收益,是行为参数化:同一个 Agent 定义,可以靠切换配置,在不改代码的情况下,给不同客户或者不同项目提供定制化的 Prompt、工具集和终止策略。
2.2 工具调用协议与容错策略
Agent 框架的第二个核心抽象是 Tool。这块如果设计不好,后期加工具的时候,会让每个人都抓狂。我在实践中总结了一套比较稳的工具协议:
- 工具必须描述清楚自己的“输入 Schema”,通常用 JSON Schema;
- 工具必须声明自己的“副作用类别”:只读、写入、副作用未知;
- 工具执行结果必须有标准包装:成功、业务失败、系统异常,三者必须区分开;
- 工具必须声明超时时间和幂等性策略。
from dataclasses import dataclass from enum import Enum class ToolEffect(Enum): READ_ONLY = "read_only" WRITE = "write" UNKNOWN = "unknown" class ToolResultKind(Enum): OK = "ok" BUSINESS_FAILURE = "business_failure" SYSTEM_ERROR = "system_error" @dataclass class ToolResult: kind: ToolResultKind data: dict duration_ms: float我踩过最大的坑,是把业务失败当作系统异常来处理。结果就是:LLM 看到工具返回的报错后,开始“自作聪明”地反复重试同一个会业务失败的操作,白白浪费 token 和时间。后来我强制工具返回三态结果后,LLM 对失败原因的判断变得精确多了,重试策略也能按类别独立配置。
2.3 记忆的三种存储粒度与回收策略
框架里记忆管理这个抽象,一定要一开始就做进去,不要等到“上下文不够用”了再来补。我的记忆设计分三种粒度:
| 记忆类型 | 生命周期 | 存储介质 | 使用场景 |
|---|---|---|---|
| 会话级工作记忆 | 单次会话 | 内存(随 Agent 实例) | 当前对话的轮次摘要 |
| 任务级记忆 | 一个任务从开始到结束 | Redis 或数据库 | 多轮交互的任务参数、中间结果 |
| 长期记忆 | 跨会话、跨用户 | 向量库/知识库 | 用户偏好、历史决策 |
回收策略上,我建议做好关键信息保留,而不是做全量覆盖。比如:用户请求原文不要丢、已经确定的业务参数不要丢、失败的尝试记录不要全部保留(但保留失败原因要点)。这三条是长期跑业务后总结出来的,确实很实用。
3. 编排层:单 Agent 循环与多 Agent 协作的执行骨架
有了四个核心抽象之后,真正让框架活起来的是编排层。这一层负责回答两个问题:单个 Agent 内部怎么一步步执行?多个 Agent 之间怎么协同完成复杂任务?
3.1 单 Agent 主循环:九步执行链路
单 Agent 的执行循环,是整个 Agent 框架的心脏。我把它拆成了清晰的九个步骤,每一步都有自己的职责,方便出错时定位:
- 构建会话上下文:从 Memory 拉取当前会话的历史和状态;
- 渲染系统提示词模板:注入上下文变量和业务背景;
- 组装用户消息:把当前输入与系统提示词拼接成模型输入;
- 调用大模型 API:带上工具定义(如果模型支持工具调用);
- 解析模型输出:判断是普通的文本回复,还是包含工具调用请求;
- 执行工具调用:调用对应工具,并拿到三态结果;
- 评估终止条件:检查是否达到该 Agent 的终态,或者是否需要中转给别的 Agent;
- 状态回写:把本轮结果写入 Memory,更新上下文变量;
- 循环继续或返回:未终止则回到第 2 步,已终止则返回最终输出。
这套循环看起来不复杂,但设计上有一个容易被忽视的点:第 2 步的“重新渲染系统提示词”。如果你在每一轮循环里都用同一套静态提示词,Agent 很容易在前几轮就跑偏。我的做法是每一轮都把“当前已执行过的工具结果摘要”注入提示词的“当前状态”区段,相当于给 Agent 一份“实时战报”,效果比让它自己揣测好得多。
3.2 多 Agent 协作:从链式调用到 Swarm 式 Handoff
说到多 Agent,就绕不开最近很热的 Swarm 框架概念:Agent、Handoff 和上下文变量。如果要用一句话总结 Swarm 的贡献,那就是把多 Agent 协作从“链式管道”变成“智能体之间互相转交主导权”。
我的框架吸收了这个思路后,多 Agent 协作模式从原来的一种变成三种:
- 链式流程编排:适合固定顺序的业务,比如工单审核——初审 Agent 完成后,结果转给复审 Agent;
- 路由分发编排:适合意图明确的任务,比如用意图识别 Agent 判断该把请求交给客服 Agent、售后 Agent 还是技术支持 Agent;
- Handoff 式编排:适合需要动态决策的任务,主 Agent 在与用户交互的过程中,根据对话内容自主决定转交给另一个 Agent,并且被转交方可以拿到前序的上下文变量。
3.3 Handoff 的元协议设计:消息即路由
Swarm 里最让我觉得有价值的设计,是“Handoff 就是消息的一种特殊类型”这个理念。传统做法是在两个 Agent 之间显式定义一条“路由”,比如 if 检测到退款意图 then 跳转到退款 Agent。这个做法僵硬、难以维护,而且绕过了 Agent 本身的自主决策权。
我把 Handoff 设计成 Agent 输出结构中的一个可选字段:
{ "reply": "好的,退款问题我来帮您转接给专门负责的同事。", "handoff_to": "refund_specialist", "context_updates": { "customer_tier": "vip", "order_id": "SO-2024-00123", "refund_reason_collected": true } }框架收到带有 handoff_to 字段的输出后,会做三件事:先保存上下文变量,再调用目标 Agent 的系统提示词渲染,最后把“当前待处理的问题摘要”作为输入消息传给目标 Agent,让目标 Agent 能从对话中间继续接手,而不是对用户说“请问你有什么问题”。
这个机制让我砍掉了大量硬编码路由逻辑。新的 Agent 只要注册一份“我能处理什么请求”的描述信息,就能被其他 Agent 在需要时转接过来。新增业务 Agent 的速度提升相当明显。
4. 上下文变量与会话状态:避免上下文爆炸与状态漂移
我把上下文管理单拎出来,是因为在实际场景中,上下文问题比模型选型问题更致命。很多 Agent 跑着跑着就对话混乱、重复提问、丢失关键参数,十有八九是上下文变量设计得不合理。
4.1 会话状态的分层设计
我把 Agent 运行时的状态分成三层,任何变量都必须属于其中一层,不允许“游离变量”:
- 请求级(Request Scope):单次用户输入的携带参数,比如用户当前的提问文本、附件ID;
- 会话级(Session Scope):整个会话过程中持续生效的变量,比如用户身份、当前选中的业务ID、语言偏好;
- 全局级(Global Scope):框架级配置和业务全局参数,比如默认超时时间、系统版本号。
分层的价值体现在被 Handoff 转交时:请求级变量自动丢弃,会话级变量跟着走,全局变量常驻。这样就天然防止了“ A Agent 留下的脏变量污染 B Agent 的判断”。
4.2 上下文变量命名空间与生命周期
命名空间这块,跟大多数人的直觉不一样——不是“为了整洁”,而是为了“防事故”。假设两个 Agent 都往上下文里写了一个名为“status”的字段,主 Agent 读到的状态到底是哪个?如果你不在框架层面强制命名空间,这个问题迟早会变成线上事故。
我制定了两条硬规则:
- 每个 Agent 写入上下文变量时,必须带前缀,格式是 agent_name.variable_name;
- Agent 只能声明“需要的输入变量范围”,框架会在 Handoff 时自动剪裁上下文,把无关 Agent 的变量过滤掉。
这两条规则让上下文变量从一开始就处于可控状态。新 Agent 加入时,只需要声明自己依赖哪些前缀下的哪些字段,不会碰它不该碰的东西。
4.3 压缩策略:什么时候丢东西、什么时候留东西
上下文窗口是有限资源,压缩策略直接决定 Agent 长跑后的智商水平。我试过简单的“长度超标就截断最早消息”,结果非常糟糕,Agent 会把早期最重要的用户意图给忘掉。后来我采用的策略是“三分类处理法”:
| 内容类型 | 处理策略 |
|---|---|
| 核心不可变信息(用户ID、业务ID、最初意图) | 永久保留,每次压缩时单独固定 |
| 过程性信息(工具调用记录、中间推理) | 摘要化压缩,只保留结论与关键数据 |
| 噪声信息(闲聊、重复表达、失败重试细节) | 直接丢弃 |
通过这套策略,我的 Agent 在跑长流程任务时,token 消耗降低了大约 40%,准确率反而更稳定了。
5. 工程化落地:错误处理、可观测性与测试策略
架构设计得再漂亮,最终要过工程化这道坎。我遇到过不少框架,Demo 阶段天下无敌,上生产后寸步难行,问题基本出在错误处理、可观测性和测试这三个环节。
5.1 错误分类与重试边界
大模型应用和传统后端有个很大的不同:错误来源多,而且很多错误没法从状态码上判断是否值得重试。我把错误分成三类,并配置了不同的处理策略:
| 错误类型 | 典型场景 | 处理方式 |
|---|---|---|
| 可重试瞬时错误 | 网络超时、限流(429)、5xx | 指数退避重试,最多3次 |
| 不可重试输入错误 | 参数校验失败、工具 Schema 不匹配 | 记录日志后返回给 LLM 要求修正 |
| 业务不可重试错误 | 业务校验不通过、用户取消操作 | 立即终止当前分支,不重试 |
这里有一个值得留意的教训:不要把“LLM 输出格式非法”归类为可重试错误。我第一次遇到模型输出 JSON 解析失败时,天真的对这次调用做了整体重试,结果白烧了一轮 token,模型再次输出的还是坏的。正确的做法是:把解析失败当成“反馈信号”重新输入给模型,告诉它“你刚才的 JSON 格式不对,请重新输出”,往往第二次就是对的。
5.2 可观测性:把 Agent 的“思考过程”变成可审计日志
Agent 应用最大的运维难点,是它不像传统的 B/S 架构那样“请求-响应”清晰直接。用户问一句,背后可能发生了五次模型调用、四次工具调用、一次 Agent 转交,每一步都可能出错。如果没有高质量的日志,排查问题纯靠猜。
我在框架里设计了结构化追踪体系,每一次运行都记录如下信息:
- 运行 ID(Run ID)与父运行 ID,用于串联整条处理链;
- 每个 Agent 的输入摘要与输出摘要;
- 每一轮 LLM 调用的 token 数、耗时和延迟;
- 工具调用参数和结果摘要(敏感字段脱敏);
- 上下文变量的变化记录;
- 终止原因:正常完成、达到最大轮数、被上/下游 Agent 转交、异常中断。
有了这套数据,我可以回到任何一次线上会话,把它按时间轴重放出来,看清 Agent 每一步在想什么、做了什么。这才是真正意义上的“可解释性”——不是模型自己解释“我为什么这么回”,而是我们能看到它走过了哪条执行路径。
5.3 测试策略:从单测到场景回放
Agent 框架的测试不能走传统应用的纯单测路线,因为模型输出有随机性。我最终采用的测试金字塔是这样:
- 第一层:组件单测。用 mock 的 LLM 响应来测工具注册、上下文渲染、状态回写这些纯逻辑;
- 第二层:黄金数据集回归。准备 50~200 条典型业务输入和期望输出,每次框架改动后跑一遍,记录通过率;
- 第三层:场景回放测试。把线上记录的真实会话序列收集为回放用例,用历史日志驱动框架重放,检验行为一致性。
这套体系让我的框架在快速迭代的同时,敢于小步发布,不会每次都心惊胆战地担心线上崩掉。
6. 我的实践体会与下一步演进方向
文章写到这里,技术上把框架的核心架构都过了一遍。最后说一点我在实际构建和使用这套框架时的真实感受,也算给想自己动手的读者一盏灯。
架构设计这件事,在我看来最难的并不是某个复杂的算法,而是忍得住不贪多。Agent 框架特别容易陷入“因为 AI 什么都能干,所以框架什么都要支持”的幻觉里。我自己就吃过亏:一开始加了视觉记忆、语音交互、异步多模态调度,结果核心的纯文本工具调用链路反而不够稳。后来砍到只剩四个核心抽象和三个协作模式,一切都回到简单可靠的状态。
在上下文管理上,我用“三分类处理法”之后,长任务的稳定性和 token 成本都明显改善。在工具协议上,强制三态返回让失败归因变得非常清晰,这是 Agent 能做出正确处理决策的前提。这些经验都来自一次次线上事故和现场排查,不是靠理论推出来的。
如果接下来你要扩展这套框架,我建议优先做两件事:一是给上下文变量和运行追踪加上持久化存储的接口,让整套状态管理可以跨进程、跨服务;二是沉淀你的黄金数据集,当你开始积累到一千条以上真实业务回放用例时,框架演进的速度会快得超乎你的想象。
Agent 框架构建这条路,入门靠 Demo,真正深入靠架构设计。这一节把整体架构定了性,后面几节我会顺着这条主线,逐层展开 Agent 定义、执行循环、编排机制和上下文细节的代码实现。跟着一路下来,你最后拿到的会是一套真正能上生产的 Agent 体系,而不是一堆零散的脚本。