多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从零搭建Agent框架:核心架构设计与工程实践

从零搭建Agent框架:核心架构设计与工程实践 很多做 Agent 开发的朋友一开始都喜欢直接拉一个现成的开源框架比如 LangChain、AutoGen 之类的把模型一接、工具一挂就跑起来。这本身没啥问题但等你真要在生产环境里跑一个 Agent 项目或者要针对自己的业务场景深度定制的时候往往会发现一个尴尬的事实框架帮你做的事很多都是你不需要的而你需要做的事框架又很难给你足够的自由度。我自己在前面几个项目的实操中踩了不少这种坑。所以在复盘和重构之后我决定自己从零搭一个 Agent 框架的骨架第七章这部分内容就是专门讲清楚这个骨架是怎么设计的。这篇博文我会把整体架构设计的思路完整拆开来讲包括组件划分、数据流设计、并发与容错处理以及一些实际落地时容易忽视的细节。这篇文章面向的是已经熟悉大模型 API 调用、写过一些 Prompt 调优、但还没有系统梳理过 Agent 框架结构的同学。1. 框架架构设计的起点先搞清楚边界1.1 需求边界决定了框架的复杂度动手设计框架之前先别急着写代码。我见过很多团队一上来就画了一个很复杂的分层架构图结果卡在第一步——连 Agent 要解决的核心问题边界都没理清楚。什么是 Agent 框架的功能边界我的建议是往下拆解几个问题模型能力层你是只用单一模型还是要兼容多个模型如果用多个模型是不同场景切换还是同一场景下做模型路由工具调用层Agent 需要调用哪些外部工具调用方式是 HTTP 接口、本地函数、还是执行一段脚本记忆管理层Agent 需要维持多长的对话上下文是单轮独立还是跨会话持久化要不要做向量检索任务编排层Agent 是只处理单步任务还是要拆解成多步计划、循环执行我在自己的项目里曾经因为一开始没有明确任务编排的复杂程度导致后面所有代码几乎推翻重写。当时我的需求其实很简单只是让 Agent 能调用一个内部的搜索接口但我在架构里先实现了完整的多步任务规划器反而让简单场景跑得又慢又别扭。后来我在做一个涉及多轮用户交互的项目才真正意识到编排层的必要性。所以我的结论是架构设计不是越复杂越高级而是越贴合你的业务场景越合适。你可以先按最坏情况做设计预留但一定要把复杂度隔离在某个模块内部不能让它污染整个框架。1.2 打包工具的困惑为什么大多数开源框架并不是直接可用谈到 Agent 框架很多新手的第一反应是找开源框架但往往会在选型上陷入困境。以 LangChain 和 AutoGen 为代表的大而全框架变量与抽象过多。我目前在使用一个核心功能良好的框架但由于它的Agent Harness设计模式要求将推理循环与工具调用逻辑解耦导致框架需要在很多环节上对数据结构做强制约定。这种强约束在简单场景下是过度的在真正复杂的场景下又不够灵活你经常要绕过框架本身去写一些很丑的工作区代码。再比如一些微框架像最近社区热度很高的一些轻量方案它们做得很轻、很灵活但是自己得从零写的东西就多了。我在早期做 Agent 项目的时候曾经在一个轻量框架和重框架之间反复横跳。轻量框架一开始上手挺快但真的到要做记忆持久化的时候发现还是得自己造轮子。我最后一次下定决心自己搭框架就是因为在用现成框架的时候发现我在调试一个模型格式输出与下游解析不匹配的问题需要不停绕过框架的校验逻辑这种沟通成本和调试成本让我意识到与其在别人提前做好的抽象中挣扎不如在我的场景上做十个更准确的选择。于是我把平行比较的重心从用哪个框架转移到了我们需要哪些抽象层级——这就是做架构设计的一个实际起点。1.3 架构设计中的 Harness 与 Agent 到底应该怎么理解在 Agent 开发圈子里面Harness 和 Agent 这一对概念容易被混淆。结合我自己的理解Agent 指的是模型加上提示词和工具定义所组成的智能执行体而 Harness 更像是整个运行时的脚手架包括输入输出规范、循环控制、日志追踪、安全过滤、错误恢复这些辅助能力。你自己搭框架的好处就是能把这两者彻底解耦。我拿一个项目里的场景举例。有一次我需要让 Agent 在一个独立的沙盒进程里执行代码沙盒进程崩溃了我不希望整个 Agent 跟着死掉但如果是用某些开源框架这两个概念是绑在一起的一旦底层运行环境出问题上层 Agent 状态也就悬空了。后来在我自己的框架里我把 Harness 设计成了两个独立进程之间的通信代理Agent 的决策在主进程里完成而在沙盒里的执行完全隔离这种隔离能力就是靠 Harness 和 Agent 分离带来的。所以如果你也在设计自己的 Agent 框架我建议在你的架构脑图中一定要把决策执行体和运行时支撑环境作为两个独立模块来设计。这样无论是后期的水平扩展、容灾恢复还是替换底层模型改动面都会被控制得很小。2. 整体架构的核心模块与层次划分2.1 分层设计的原则好的架构设计在我看来核心就是一条高内聚、低耦合数据流单向流动。Agent 框架也是一样。我给自己框架定的分层是这样的接入层负责处理用户的输入、接入不同渠道API、命令行、IM Bot 等编排层负责理解任务、拆解步骤、决定要调用哪些工具执行层负责真正调用模型、调用工具、执行代码记忆层负责存储和检索对话历史、长期知识、临时变量基础设施层负责日志、监控、缓存、消息队列、安全控制每层之间只依赖接口不依赖具体实现。比如接入层完全不需要知道记忆层是用的 Redis 还是内存它只需要知道有一个 Store 接口可以往里面写一条消息就够了。2.2 核心模块拆解模型网关Model Gateway模型网关是所有模型调用的统一入口。为什么要单独抽一层因为你的代码里不应该到处直接出现openai.ChatCompletion.create()或者对某个模型 SDK 的直接调用一旦你从 OpenAl 切到 Anthropic或者从 GPT-4 切到 DeepSeek你会发现所有写死的点都要改。我设计的模型网关至少需要提供以下几个能力统一的请求协议不管底层是哪个模型网关都能接收一个统一的 Prompt 输入返回一个统一的响应结构超时与重试模型调用最常见的错误就是限流和超时网关要做指数退避重试成本与 token 统计每次请求记录输入、输出 token 数方便做成本核算模型路由根据任务类型或规则将请求转发到不同的模型我在实际项目里遇到过一个问题用模型 A 处理简单的分类任务时效果很好且便宜但模型 B 处理复杂推理更棒。如果我把模型选择逻辑全部写在业务代码里代码里就会到处充斥如果任务复杂度高就用模型 B这种逻辑。后来我把路由策略完全收进模型网关业务侧只传一个task_priority字段网关根据阈值自动选模型业务代码清爽了很多。2.3 核心模块拆解记忆管理层记忆是 Agent 和普通 API 调用最大的区别。我把记忆分成两种工作记忆和长期记忆。工作记忆是指当前这次任务执行期间所有需要保留的上下文基本可以靠一个上下文数组或字典来维护在单次执行结束后清空。长期记忆则是跨会话、跨任务需要保留的信息比如用户偏好、历史事实、领域知识通常使用外部存储加向量检索。工作记忆设计上要控制膨胀。就算是最强的模型面对超长的上下文也会产生性能退化而且 token 费用也是不可忽视的。所以在每次新一轮执行之前我都会做一个压缩摘要的操作把上一轮完整历史丢给模型做一次 summarize保留核心要点再拼接到下一轮的上下文里。长期记忆设计上要区分事实型和经验型。事实型记忆直接用数据库存储键值或结构化记录经验型记忆才需要用到向量库。我之前用向量库存所有对话历史结果查询召回效果很差。后来改成了只把确认过的事件、用户明确表达的偏好向量化效果才上来。2.4 核心模块拆解工具注册与调度层工具层是 Agent 跟外部世界交互的桥梁。我的设计里工具层包含两个部分工具注册中心和工具调度器。工具注册中心是一个全局注册表每个工具需要声明这些信息工具名称与描述输入参数结构用 JSON Schema 定义是否异步执行可选的执行超时时间权限校验逻辑工具调度器则负责根据模型的输出决定调用哪个工具。这里我特别强调一点千万不要直接信任模型生成的参数。即使是在 function calling 模式下面模型生成的参数也有可能出现格式错误、类型不对、或者内容并不符合工具的要求。所以调度器的第一件事就是做参数校验校验通过才真正执行。我在一个物流调度项目里因为模型生成的时间参数格式踩过坑。模型输出了下午2点但我的工具接口只接受 ISO 8601 格式。调整调度器加强了校验和转换逻辑之后这类问题才算彻底解决。所以工具层的设计逻辑不只是有了能调用就完了而是有、有校验、有容错才算真正完成。3. 一次 Agent 调用的完整生命周期3.1 从请求到响应链路数据流详解把架构设计的模块拆开讲完之后我们串一下一条完整的调用链路这样架构才是活的。假设用户从接入层发来一个请求帮我查一下昨天订单量最高的三个商品然后生成一份简短的总结报告。第一站是编排层。编排层会先对用户请求做意图分析。在这个例子中意图是查询数据并生成报告这是一个复合任务编排层会把它拆成两个步骤第一步是调用数据查询工具第二步是根据查询结果生成报告。拆分完之后编排层把每一步的执行需求交给执行层。执行层开始第一步调用模型做工具选择。模型看到工具列表里有query_order_data和generate_report两个工具它会判断第一步需要使用query_order_data这个工具同时生成参数比如date昨天, limit3, sort_byquantity。工具调度器拿到这个请求后先校验参数格式然后把请求转发给实际的业务服务。这一步完成后结果被写回工作记忆。然后执行层开始第二步再次调用模型这次用户消息变成了用户原始请求加第一步查询结果。模型直接生成总结报告文本。这份报告作为最终响应沿原路径返回给接入层最终回复给用户。3.2 上下文组装像组装乐高一样拼出 Prompt上面链路里最关键一批中间件就是上下文组装。我在设计时给 Agent 定义了一套观察、思考、行动、总结四步循环机制而上下文组装则是整套循环的大脑。它的职责是决定每次模型调用时要给模型看哪些内容、以什么顺序展示。我的组装模板大致是这样的顺序系统指令固定系统 prompt定义你的 Agent 人设和边界工具描述模型需要知道当前能调用哪些工具长期记忆摘要与当前任务可能相关的历史信息对话历史最近N轮的工作记忆当前任务和中间结果当前正执行到的步骤以及已经获取的中间数据有一个细节组装器还要为每部分内容打一个权重标签避免重要信息被中间结果挤掉。我在项目里遇到过一个问题对话历史太长导致模型在后面调用工具的时候完全忘了前面用户说的原始需求。后来我做了关键信息提炼模块——每次组装的时候不是简单拼接全部历史而是把聊天历史里用户明确的指令用提取器单独摘出来排在前面。这样的处理既保留了信息又显著提升了模型在工具调用中的准确率。3.3 模型输出标准化从自然语言到结构化指令前面提到模型输出不能直接信任这里展开说。尤其是在工具调用场景下我会强制模型输出 JSON 结构而不是让模型随意发挥。具体做法是在系统 Prompt 里明确要求模型在工具调用步骤输出一个固定 JSON Schema并且加了校验和纠错模块。我在项目里的做法是在模型返回之后用 JSON Schema Validator 校验一遍如果格式不合法我不会直接报错而是把错误信息反馈给模型让模型自我修正一次。例如您的输出不符合格式要求: 缺少必要字段action。请重新在 JSON 代码块内输出符合格式的响应。这招在大多数现代模型上都很管用。修正次数限制为两次超过两次则终止循环抛异常这样可以避免模型陷入死循环导致 token 消耗失控。4. Agent 框架的工程化设计并发、容错、安全4.1 如何扛住高并发从排队到并发调度很多做 Agent 项目的人跟我聊到一个话题就是AI Agent 怎么扛并发。这个问题本质上是两个问题第一模型 API 自身的并发上限。即使是国际上性能不错的大型模型服务商也会遇到 Rate Limit国内大模型厂商的服务也一样有限流。一旦你的 Agent 同时收到 100 个请求直接在请求线程里同步调用模型 API很快你的会用完配额或者被报错。第二Agent 执行任务时通常是长时间占用资源与普通应用请求的秒级响应不同Agent 可能要几十秒甚至几分钟才能完成一个任务。这就不能像普通后端那样每个请求一个线程拉到底。我的做法是引入两层入口通过消息队列比如性能较强的 MQ 组件接收所有 Agent 任务调用方立即拿到一个任务 ID。核心调度器维护一个 Token Bucket控制并发调用模型的数量上限。每个 Agent 任务会进入一个可恢复的工作流在执行过程中它会因模型调用阻塞但不占线程。这里我要强调一个经验不要把并发数设得和模型限流的上限一样高。比如模型限流是每分钟 100 次调用你最好把 Agent 调度器的并发控制在 80留出 20% 的缓冲应对重试和突发。我在项目上曾经因为并发放得太满导致请求一动就触发限流重试重试又占用额度最后出现大面积超时这种恶性循环非常吓人。4.2 错误处理当模型输出胡言乱语和拒绝执行时Agent 开发中一个非常令人头疼的问题就是agent execution terminated due to error这类偶发异常。当然这个错误在不同的框架成因不同但大体上都指向某个循环中的关键节点抛错导致整个执行链断掉。我自己的框架把我设计成了三个层次第一层单次调用层。LLM 调用因为网络抖动或者服务端 500 错误直接重试指数退避。第二层工具执行层。工具抛业务异常我不做自动重试而是把错误信息反馈给 Agent模型让 Agent 决定是换一种方式还是告知用户。这一层的设计思路是工具错误不一定是模型能解决的但如果模型能看到错误信息它往往能给出更合理的替代方案。第三层编排层。如果 Agent 在一连串计划执行之后发现目标仍未达成进入最外层兜底逻辑放弃任务但把中间所有执行过程总结成结构化日志返回给用户。这样至少用户知道任务是在哪个环节失败的而不是只有一个黑色的错误码。对了关于终止循环也有一个经验。很多人设计的 Agent 循环没有步数上限这非常危险一个简单的循环可能让模型在那里反复调用同一个工具几十次。我会强制任何 Agent 任务都有一个 step 上限默认 15 步复杂任务 30 步超出即终止并返回当前进度。4.3 安全设计Agent 的权限边界与沙盒聊 Agent 安全很多人第一反应是担心模型被越狱、被注入异常指令。但在框架架构层面Agent 安全还有更大的一层问题工具权限和系统边界。基于我这个理解在设计 Agent 工具调用时我引入三个安全机制权限令牌每个 Agent 任务启动时会根据用户身份生成一个临时签名令牌这个令牌只授予该任务所需的最低权限任务结束立即失效。工具白名单Agent 可用工具列表不是固定的而是根据当前任务级别动态生成的。用户问天气和用户要求删除服务器文件这两类请求下可调用的工具列表完全不同。执行沙盒所有可以执行代码或系统命令的工具都要在一个隔离的容器中运行。我在架构里给 Agent 的工具排了一档比如执行 Python 代码这类工具只会运行在沙盒进程里沙盒里没有任何生产密钥甚至访问公网都受限。有一次我测试了一个恶意提示词注入攻击案例用外部输入的方式成功让模型生成了一个删除用户目录下的所有文件的指令。如果这个 Agent 工具权限不是分层隔离的后果就严重了。但在我设计的权限模型下虽然模型产生了这个指令但工具白名单里根本不存在这个工具所以系统直接拒绝了执行。这个实战案例说明安全设计在 Agent 框架里不是可选项而是必须内嵌在架构里的硬要求。5. 实操落地从零开始搭建你的 Agent 骨架5.1 最小可行架构的代码框架理论讲了这么多还是要落地。下面我给出一个我常用的最小文件结构基于 Python 编写读起来非常自然agent-framework/ ├── agent/ │ ├── __init__.py │ ├── core/ │ │ ├── context.py # 工作记忆与上下文管理 │ │ ├── gateway.py # 模型网关 │ │ └── memory.py # 长期记忆抽象接口 │ ├── tools/ │ │ ├── registry.py # 工具注册中心 │ │ └── dispatcher.py # 工具调度器 │ ├── orchestrator/ │ │ ├── planner.py # 任务规划器 │ │ └── runner.py # 循环执行器 │ └── harness/ │ ├── security.py # 权限校验与沙盒控制 │ └── logger.py # 日志追踪 ├── config/ │ ├── settings.yaml # 全局配置 │ └── tools.yaml # 工具声明列表 └── main.py # 接入层入口这个结构里我特意把harness和core分开了。harness里面的组件不依赖于任何具体的模型和工具它们是通用的运行时支撑逻辑这样做保证了 Agent 本体是可替换的、可测试的。5.2 工具注册与路由的配置化实现工具注册我建议采用配置文件加装饰器双轨制。核心工具用 Python 装饰器直接注册# tools/weather.py from agent.tools.registry import register_tool import httpx register_tool( nameget_weather, description获取指定城市的当前天气信息, parameters{ type: object, properties: { city: {type: string, description: 城市名称} }, required: [city] }, timeout10, ) def get_weather(city: str) - str: resp httpx.get(fhttps://api.example.com/weather/{city}, timeout5) resp.raise_for_status() return resp.text对于外部系统接入的工具则是配置文件驱动这样运维和交付同学可以不改代码就加新工具# config/tools.yaml tools: - name: query_order_data endpoint: http://internal-service/order/query method: POST auth: type: signature params_schema: type: object properties: date: {type: string, format: date} limit: {type: integer, default: 10}一开始可能会觉得写配置比写代码麻烦但放到团队协作和维护视角配置分离的优势就非常明确了。5.3 编排层的循环控制器实现思路循环执行器是 Agent 的核心心跳。我推荐一个特别简化的实现思路正常执行循环组装上下文用户原始诉求、对话历史、中间结果、工具列表调用模型判断模型输出的类型如果是最终回答记录结果并结束如果是工具调用请求校验、执行、记录结果回到步骤 1如果输出不合法反馈纠错信息回到步骤 1如果步骤数到达上限强制终止并返回已完成的中间结果这个循环最大的优点是通用同时天然支持了各种真实复杂的嵌套与循环场景。你可以在执行过程中随时把中间结果写入日志和记忆模块从而实现断点续跑。5.4 配置管理与环境隔离Agent 框架通常会用到多个密钥、多个模型 API、多种外部服务地址我强烈建议从一开始就引入环境配置管理而不是简单地在代码里硬编码。入门的做法是在项目根目录放一个.env.example文件把必须的变量名写好但把真实值隐藏。然后在代码里统一读取环境变量这样不同的开发环境、测试环境、生产环境切起来非常轻量而不会出现本地调试好好的上生产就各种密钥失效或者请求错地址的情况。我做了一个版本化的配置中心每次发布或者变更模型参数都符合审计要求这在规模化部署时非常重要。6. 常见问题排查与避坑实录6.1 循环失控token 消耗爆炸现象Agent 反复调用同一个工具 20 多次每一次调用都返回一样的结果但模型还是坚持继续调用。排查思路先看日志确认工具返回结果是否被正确写到了工作记忆。往往这一步已经出了问题不是模型的问题是你把工具结果放在上下文里放在了不上一个位置模型其实看不到自己上一次调用已经拿到了结果只能重新发起调用。其次你要确认在循环过程中没有触发结果同步错误。在我的项目里只要工具执行失败我不仅把错误信息写进去还会用可视化标记把这个工具刚刚已经调用过了结果如下这句总结性提示放在最前面。模型看到这句之后基本就不会再钻牛角尖。根治方案永远在循环控制器里设置步数上限并对同工具连续调用次数做单独限制。比如连续调用同一个工具 3 次结果不变直接判死循环并终止。6.2 模型频繁输出 JSON 格式错误现象在代码场景不可避免总会遇到 JSON 解析失败尤其是在使用一些中文模型、且没有强约束力的函数调用模式时。排查思路第一步判断你是不是用了可靠的 JSON 函数调用特性如果用了还是乱输出那就是你输入的 System Prompt 里给的 JSON 示例与模型训练分布不一致。表里给出的两个示例质量就非常关键。根治方案我建议在 System Prompt 里给少而精的示例。给出一个完全贴合你目标场景的例子同时用一个明显的标记在输出格式周围圈定代码块并在解析逻辑中支持从代码块中提取。宁可多写一点代码去容忍模型的各种格式怪癖也不要期望模型能记住严格的 JSON 输出这个指令实测下来直接解析代码块内部比直接让你转成字典更可靠。6.3 记忆模块查询效果好差怎么调都召回不出相关内容现象长期记忆的向量检索结果与当前任务完全无关。排查思路往往不是检索算法出问题而是记录记忆的时候就没做好分割。比如用户说了一长段话你把这整段话都丢进了向量库里面包含了今天天气不错和我下周要出差到上海两件事检索下周出差地时整体向量被无关信息严重稀释相关性的评分自然低。根治方案记忆入库前做拆分与实体提炼。我直接用一个小模型来做信息抽取把用户会话中的结构化事实时间、地点、人物、动作抽取成三元组并附带原始句子一起入库。在检索时我就既做语义检索也做关键词匹配两种结果交叉合并后按得分排序效果提升非常明显。6.4 并发测试中大量 429 限流错误现象并发一上来模型 API 报 429而且重试之后还是报。排查思路先确认你的令牌桶与底层限流之间的配比。如果你给框架配的并发是 10但底层 API 限额是每分钟 10 次调用显然超出很多。再确认重试策略是不是导致了二次拥塞。我当时测试时用的是固定间隔 1s 重试结果高峰期 50 个线程同时重试把下一分钟的额度也打满了。根治方案全局引入动态限流器根据最近一分钟内的请求成功率动态调整当前并发。成功率低于 90% 时并发降低 20% 并延迟重试至指数退避成功率回升后再开放并发。这样整个系统在限流边缘能保持稳定吞吐而不是来回震荡。6.5 工具调用有副作用不能随便重试现象调用了一个创建订单的工具请求超时了你按原参数重试了一次结果创建了两个订单。这是最常见的工程事故。排查思路工具层必须区分是幂等还是非幂等。幂等可以通过参数本身存在比如订单号来保证非幂等则要引入事务或对账机制。根治方案在工具注册中心增加一个idempotent字段。非幂等的工具框架在调用前会生成一个全局唯一的request_id下游服务接收到相同的request_id时会直接返回首次执行的结果从而保证重复调用也不会重复执行。如果你还没有做这一步建议尽快把这个机制加到你的服务端。7. 架构设计的后续规划注册中心与插件生态7.1 下一步演进Agent 的注册中心框架搭好之后我想要进化到更宏大的方向——Agent 的注册中心。这一类比微服务架构设计里的服务注册中心不过是为 Agent 提供一个统一入口让 Agent 像一个服务一样被注册、被发现、被调用。我已经在前面的设计里预留了 Agent 编排层和接入层之间的协议可以比较平滑地增加一个注册中心模块。当多个 Agent 在同一套基础设施内运行注册中心把不同 Agent 的能力、状态、负载作为元数据集中管理上层业务可以像调用一个工具函数一样调用不同的 Agent。7.2 善用插件生态扩展与兼容在自研框架推进了一段时间后我开始关注插件生态。早期做一些自用 Demo 时完全不理会扩展生态后来发现很多重复造轮子是因为没有复用社区的经验和组件。现在我的策略是核心调度与循环部分自研插件化接入的生态组件尽量直接匹配社区已有的标准工具协议这样既保有自己的控制力又避免重复造轮子。我在项目里已经基于这套插件机制接入了一个第三方数据看板工具因为它的 API 定义符合协议规范几乎零代码就接上了。这让我深刻体会到框架预留规范化的接口协议其收益会随着规模增大而指数级增长。7.3 自主 Agent 框架与敏捷协作最后聊一个额外的现实。我在这几次 Agent 框架演进中发现一个关键认知Agent 框架不只是一个技术项目它更像一个敏捷开发方法因为框架本身必须跟随 Agent 的需求变化快速迭代。一个可嵌入的模块化插件结构比一个固化的大一统设计更能适配快速变化的需求。这也是我做这套架构设计最重要的心法所有的架构决策都必须服务于变化。如果某一个设计让你在后续每次需求变化时都感觉很痛苦那么这个设计就是错误的反之如果你改一个模型、加一个工具、插一个新的编排策略都只需要在一个局部位置进行修改那么恭喜你你的 Agent 框架整体架构已经走在了正确的路上。
返回列表