资讯动态

从零构建Agent框架:核心架构设计与编排实战解析

发布时间:2026/10/2 22:32:53 来源:尧图企业网站定制
聊到Agent框架我发现很多人第一反应就是“我直接调大模型API不就行了干嘛还要搞一套框架”。但等你真正上手做过几个复杂的智能体应用比如一个需要多角色协作的客服系统、一个要串联搜索加文档处理的调研助手就会发现没有框架约束的Agent开发前期很爽后期全是坑。状态散落在各个函数里、角色切换靠硬编码、上下文变量传得乱七八糟最后维护成本高到你想重写。这一章我们聊的就是怎么从头构建你自己的Agent框架并且聚焦在最核心的部分——整体架构设计。我会结合Agent框架与编排的实战经验重点拆解Swarm风格里非常实用的三个概念Agent智能体单元、Handoff交接机制和上下文变量Context Variables。这套思路最大的价值不是让你照搬某个库而是帮你建立起一种“先设计架构、再填充代码”的思维方式。只要理解了这套设计逻辑无论你后续用LangChain、Semantic Kernel还是自己写一套轮子都能做到心里有数。1. 从单体Prompt到多Agent编排架构设计的核心思路1.1 单体模式的天花板在哪里先说个我自己的经历。早前做一个内部数据分析助手一开始就一个AgentPrompt里塞满了各种角色指令、业务规则、工具说明、输出格式约束。刚开始只处理两三个场景还行但随着业务方不断提需求——要查订单、要算转化、要生成周报、还要能自动跟进异常——这个Prompt膨胀到几千字回复质量开始忽上忽下。问题出在哪儿大模型本身不是状态机它没有能力在一个超长Prompt里稳定切换多个角色并维护复杂的执行流程。你让它在数据分析师、报表生成器、告警通知员之间自由切换它大概率会在某个边界上“人格分裂”甚至把工具调用参数搞错。这就是单体模式的硬天花板单一上下文窗口内的指令冲突和注意力分散。后来我复盘真正需要的是把不同职责拆成独立的Agent让每个Agent只做一件事然后通过明确的接线方式让它们协作。这就是多Agent架构的起点。1.2 三个关键抽象Agent、Handoff、Context Variables在设计自己的Agent框架时我提炼出三个必须首先考虑的核心抽象这也是当前社区里Agent框架与编排讨论最多的三个概念Agent一个自包含的智能体单元它有自己的系统指令、可用的工具函数、模型配置以及一个名字。在框架层面它就是一切行为的载体。HandoffAgent之间转移控制权的机制本质上是把一个接力棒从当前Agent递给下一个Agent。Handoff是编排的核心它让角色切换从写死代码变成模型根据上下文动态决策。Context Variables跨Agent共享的可变上下文存储用来传递用户ID、业务参数、中间计算结果等状态信息。它是所有Agent的“公共黑板”。这三个抽象的关系非常清晰Agent负责执行Handoff负责转移执行权Context Variables负责跨Agent的数据共享。三者解耦之后你可以在不修改Agent内部逻辑的情况下调整整个系统的行为流程这就是框架带来价值的关键。2. 框架整体架构的分层设计2.1 四层架构模型我构建Agent框架时第一件事不是写代码而是画架构图在脑子里或纸上。我把整个框架分为四层层次职责关键组件接口层对外暴露统一调用入口Client、Runner、Streaming支持编排层管理Agent状态机与Handoff流程执行循环、Agent切换、终止条件能力层提供Agent可用的工具和扩展点Function Calling、RAG检索、外部API存储层维护上下文变量与对话历史Context Variables、Message Store接口层面向的是业务调用方。你在外部代码里只需要创建Agent实例然后调用类似run(agent, messages)的入口剩下的循环和交接都交给框架内部处理。这一层做得好业务代码可以非常薄。编排层是整个框架的心脏它实现了一个标准的Agent执行循环取消息、拼上下文、调模型、处理工具调用、判断是否需要Handoff、更新上下文变量、再进入下一轮。这里需要特别注意对循环次数的控制否则Agent可能在一个工具调用链里打转出不来。能力层解决的是Agent能干什么的问题。通常是通过工具函数Tools/Functions把Agent跟外部世界连接起来比如查数据库、调搜索、发邮件。在架构设计阶段要留好扩展位让后续新增工具不需要改动编排逻辑。存储层负责数据的持久化与传递。设计上要区分两种数据一种是对话消息Messages属于线性历史另一种是上下文变量Context Variables属于键值状态。两者混用是新手最容易踩的坑后面我会详细讲。2.2 为什么编排层必须是状态机驱动多Agent系统最忌讳的是用回调地狱来做流程控制。你想想如果让Agent A在处理完某个消息后通过回调触发Agent B再在回调里触发Agent C整个系统的执行路径会隐晦得没法调试。正确做法是把编排层理解为一个状态机。系统在任何时刻都处于某个Agent的控制之下当Handoff发生时状态从Agent A迁移到Agent B但控制权始终掌握在单一的Run循环里。这种设计有几个好处可观测任意时刻你都能查看当前Agent是谁、上下文变量是什么、下一轮如何执行。可中断在每轮循环之间都可以安全地暂停或终止例如设置max_turns上限。可恢复由于状态都显式保存理论上可以实现断点续跑。我在自己框架里维护了一个简单状态对象current_agent、context_variables、messages和turn_count。每次循环只修改这几个字段逻辑非常干净。3. 核心组件实现Agent、Handoff与Context Variables的落地细节3.1 Agent数据结构的定义首先定义Agent类。不要一上来就往里面塞一堆业务逻辑Agent只需要做三件事声明身份、定义行为边界、挂载能力。from typing import Any, Callable, Optional # 工具函数的通用签名 ToolFunction Callable[..., Any] class Agent: def __init__( self, name: str, instructions: str, functions: Optional[list[ToolFunction]] None, model: str gpt-4o-mini, handoff_description: str , ): self.name name self.instructions instructions self.functions functions or [] self.model model self.handoff_description handoff_description注意到handoff_description这个字段没有它非常关键。当模型需要在多个Agent之间决定交给谁时它不是靠读Agent内部指令来判断的而是靠你提供的简短描述。比如TriageAgent.handoff_description 适用于处理订单查询、退款申请等客户服务场景。这相当于给Handoff机制提供了一个轻量级的路由表。3.2 Handoff函数的设计模式Handoff在Swarm风格框架里的实现方式非常巧妙它被设计成一个返回特殊结果的工具函数。当前Agent的模型在需要切换时会调用一个工具函数比如transfer_to_billing_agent这个函数的返回值不是普通的文本而是一个包含目标Agent引用的结构体。class Handoff: def __init__(self, agent: Agent): self.agent agent def transfer_to_billing_agent(): 将用户转移到账单处理Agent return Handoff(agentbilling_agent)然后在注册函数列表时把transfer_to_billing_agent当作普通工具注册给当前Agenttriage_agent Agent( nameTriage, instructions根据用户意图分流到合适的部门Agent。, functions[transfer_to_billing_agent, transfer_to_support_agent], )为什么这样设计因为在Function Calling的协议里模型天然就会调用工具你不需要额外教它切换Agent这件事。只要当前Agent判断出用户的请求超出了自己的能力范围它就可以通过调用Handoff函数来结束自己的控制权。这对于编排层来说处理方式也非常统一如果工具调用返回了Handoff就更新current_agent并继续循环。3.3 上下文变量的传递与更新规则上下文变量在设计上要和消息严格区分。消息是线性的、一次性的对话记录而上下文变量是系统全局的、可随时读写的状态。我习惯用字典来承载context_variables { user_id: U-10086, user_name: 张三, order_to_check: SO-2024-001, escalated: False, }在调用模型前框架会把上下文变量拼进系统提示里让Agent“感知”这些状态在函数执行后框架会捕获函数返回值并更新上下文变量。这个更新过程最好做成显式的工具函数通过返回{context_variables: {order_status: shipped}}来声明更新。框架执行完函数后自动合并到全局字典中。这里我的建议是上下文变量尽量存业务数据不要存临时中间结果。比如你计算了一个中间分数转瞬就用完了就没必要存在上下文变量里存了反而污染其他Agent的视野。4. 实操构建Runner执行循环4.1 最小Run循环的代码骨架下面是一个最小可用的执行循环实现它包含了Agent框架最核心的运作逻辑。你可以把它当作框架的“操作内核”。import copy from openai import OpenAI client OpenAI() def run( agent: Agent, messages: list[dict], context_variables: dict, max_turns: int 10, ): 执行Agent循环支持Handoff和上下文变量更新。 current_agent agent context copy.deepcopy(context_variables) turn 0 while turn max_turns: turn 1 # 1. 组装系统提示把Agent指令和上下文变量注入消息 system_prompt current_agent.instructions if context: system_prompt \n\n# 当前业务上下文变量\n str(context) messages_for_model [{ role: system, content: system_prompt, }] messages # 2. 调用模型 response client.chat.completions.create( modelcurrent_agent.model, messagesmessages_for_model, toolscurrent_agent.functions or None, ) response_message response.choices[0].message messages.append(response_message) # 3. 没有工具调用说明本轮Agent会话结束 if not response_message.tool_calls: break # 4. 执行工具调用 for tool_call in response_message.tool_calls: func_name tool_call.function.name args json.loads(tool_call.function.arguments) # 找到并执行对应函数 for fn in current_agent.functions: if fn.__name__ func_name: result fn(**args) # Handoff切换当前Agent if isinstance(result, Handoff): current_agent result.agent # 在消息里记录切换动作方便追踪 messages.append({ role: assistant, content: f【系统】控制权已移交给 {current_agent.name}, }) else: # 普通工具结果作为 function message 返回 messages.append({ role: function, name: func_name, content: str(result), }) break return { agent: current_agent, messages: messages, context_variables: context, }这段代码只有十几行核心逻辑但你已经可以拿它跑通一个多Agent协作场景了。比如客服分流、订单查询、售后处理等等。我在实际项目中就是这么起步的等跑通了再逐步加缓存、流式输出、异常重试这些增强能力。4.2 参数设计的几个细节考量上面代码里有两个参数值得你多琢磨max_turns和context_variables的深拷贝。max_turns建议设置在5到20之间。设太小复杂的工具链还没跑完就被截断了设太大一旦循环出现故障会白白消耗大量API费用。我一般先用10作为默认值再对特定场景单独调整。copy.deepcopy(context_variables)是一行不太起眼但非常重要的代码。如果不深拷贝多个并列的Run调用会共享同一个字典对象前一个调用的副作用会悄悄污染后一个调用。这在并发的Web服务里是绝对的隐患。我第一次写这个循环时没做深拷贝结果线上两个用户会串数据排查了很久才发现是共享引用问题。4.3 流式输出与中断支持虽然最小框架不强制要求流式输出但真实业务中很多场景需要“打字机”效果和请求中断能力。要在架构上预留这两个能力我建议把run函数改造成生成器模式def run_streaming(agent, messages, context_variables): ... # 把每一轮模型的增量输出通过 yield 交给外部 # 外部可以随时停止迭代达到中断效果这样改动的收益很大用户交互体验更好同时如果你要在中间插入人工审核节点只需要在Generator的Yield位置暂停即可。在框架设计阶段把流式作为一等公民比事后打补丁要轻松得多。5. 常见问题与排查技巧实录5.1 Agent循环无限重入怎么办症状某个Agent不断调用Handoff函数在几个Agent之间来回弹跳直到撞上max_turns上限。其实这是多Agent系统最经典的故障。我的排查步骤在Handoff产生时打印转移日志从哪个Agent转移到哪个Agent。检查是不是两个Agent互相把对方设为Handoff目标形成乒乓效应。检查Handoff函数是否被模型误调用——比如指令里没说清楚什么情况下该转移模型就容易乱用。在代码里加防重入判断如果连续3次Handoff在同一对Agent之间发生直接终止循环并提示人工介入。有一回我排查一个问题发现某个Agent在指令里写了如果你是账单Agent可以调用转移函数交给客服Agent这本身没问题但客服Agent的指令里又写了如果需要处理账单请转移给账单Agent。于是这两个Agent在边界案例上无限互踢皮球。解决方法是把交接职责单独抽到一个Triage Agent里明确路由规则避免双向循环。5.2 Handoff后上下文差点丢失症状Agent A在工具函数里更新了context_variablesHandoff给Agent B后B完全看不到这些更新表现就是“刚查到的订单号换个Agent就忘了”。原因通常是两个一是上下文变量的合并逻辑写在某个Agent的工具函数内部Handoff发生时新Agent从全局字典读不到更新二是消息列表里没有保留中间结果B只能依赖系统提示里的上下文变量但变量压根没有传过去。我的实践是任何工具函数的返回值只要包含业务状态一律先合并到全局上下文再走下一轮模型调用。我把这个逻辑放在Run循环里也就是在步骤4执行完函数后统一做一次context.update(...)而不是让各工具函数各自为政。5.3 Function Calling参数解析失败模型在某些情况下会生成非法的工具参数比如把字符串传给需要Integer的字段。这种情况如果不对参数做校验框架会直接崩溃。我的建议是在工具执行外层包一层捕获异常的逻辑参数解析失败时返回一个错误描述给模型让模型自己修正try: result fn(**args) except Exception as e: result f调用失败错误信息{str(e)}请根据错误信息调整参数后重试。这个设计很朴素但巨有用。因为模型看到错误信息后大概率会重新生成正确的参数比直接把流程打断要好得多。注意不要打印完整堆栈给模型一是浪费token二是可能把内部实现暴露给用户。5.4 多轮对话中的历史消息无限膨胀症状上下文变量越来越大每一轮请求的token消耗直线上升响应变慢。这是运行久了必然会遇到的问题。解决办法通常是截断消息长度只保留最近N轮消息。重要业务状态提取后存入上下文变量然后从消息历史中删除原始长文本。总结式压缩用一个轻量模型对旧对话做摘要把摘要作为上下文变量传给后续Agent。我在框架里实现了prune_messages策略按最大消息数和最大token数双维度裁剪。这个需要根据你使用的模型上下文窗口来调参没有通用值但架构上一定要预留这个钩子。6. 架构演进与实践心得6.1 从手写框架到参考Swarm风格我踩过的关键分岔路很多朋友问我市面上已经有Agent框架了为什么还要自己写我的回答是自己写不是为了重复造轮子而是为了真正理解轮子内部的结构。我在构建这套框架的过程中借鉴了不少Swarm风格的理念——Agent是极简单元、Handoff是工具函数、上下文变量是全局状态。这几个理念本身就足够简洁以至于你甚至不需要封装成一个库直接写几十行代码就能在自己的项目里用起来。但要注意一点自研框架适合作为业务逻辑的骨架不适合直接处理底层并发、模型管理、Token计费等基础设施。当你把框架搭起来后完全可以把“能力层”的Tool函数替换成LangChain的工具包把“模型层”替换成你自己的接口这样既保留了架构的灵活性又享受了生态的便利。6.2 架构设计中最值得投资的三个环节如果让我说哪个环节投入产出比最高我首推日志追踪。在Run循环里每一轮都输出当前Agent名称、调用的函数、上下文变量快照调试效率提升三倍以上。测试夹具。准备一套模拟工具函数和固定上下文变量的测试集每次改动框架后能快速回归验证。降级策略。设计好模型调用失败、工具调用空返回、Handoff死循环这三类故障的兜底响应不要指望AI不出错要默认它会出错来处理。6.3 对Agent框架后续演进的个人思考按我目前在一线项目上的体感Agent框架的发展方向不会是越来越重而是越来越轻。未来会看到更多像Swarm这种强调简洁、无状态的编排风格被融合进主流工具链。上下文变量这种设计会让你觉得不过瘾——它太简单了但恰恰是简单才让它可靠。复杂的状态管理和持久化完全可以放在框架外的数据库/缓存层来搞定而不是塞进Agent内部。我自己下一步的计划是给这套框架加上异步支持和持久化消息存储让多个Run可以跨请求共享上下文。如果你也正在构建自己的Agent框架我建议你也从小处着手先跑通一个最小闭环再逐步加复杂度。等你把架构设计的这些核心概念摸透了就会发现Agent的开发其实没有想象中那么玄乎。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑