
用现成的 Agent 框架搭一个 demo 很容易:定义几个工具、塞一段提示词、跑一个循环,半小时就能看到效果。但当你要设计一个框架本身——让它能被多个团队、多种业务、多类模型复用——难度会陡然上升。
原因在于,框架设计者必须回答一组应用开发者不必回答的问题:哪些概念应该成为一等公民,哪些应该留给使用者自由组合?状态该放在哪里,如何持久化和恢复?工具调用失败算谁的责任?循环终止条件由框架定还是由业务定?可观测性应该是可选项还是内建能力? 每一个问题背后都是契约的取舍,而契约一旦发布,改起来成本极高。
更棘手的是,Agent 领域本身仍在快速演进:模型能力在变、工具调用协议在变、推理范式在变。一个好的框架不能把当下某一种范式写死,而要在变化中保持稳定的抽象。这就是本文要讨论的核心:Agent 框架的架构决策从哪来、边界怎么划、机制怎么设计。
设计框架的第一步不是画架构图,而是明确"框架管什么"。经验法则是:
框架应该抽象"所有 Agent 都会重复做的事":模型调用、消息结构、工具协议、执行循环、状态推进、错误重试、日志追踪。这些是共性,抽象出来能显著降低使用成本。
框架不应该抽象"业务特有的事":具体提示词、领域工具、业务校验规则、审批流程。这些应该通过接口留给使用者,框架只提供挂载点。
框架要谨慎抽象"仍在演进的机制":例如多 Agent 协作拓扑、复杂的规划算法。过早把这些做成框架核心,很可能在范式变化时成为负资产。更好的做法是先提供最小原语(如子 Agent 调用、消息传递),让使用者在原语上自行组合。
一句话概括:框架提供骨架和契约,业务提供血肉和策略。 判断一个抽象是否值得做,标准是它是否能让使用者少写"与业务无关的重复代码",同时又不会限制使用者表达业务意图的自由。
一个稳定的 Agent 框架,通常围绕六个核心概念构建。它们的稳定性决定了框架的寿命。
模型(Model) 是模型调用的统一适配层。它的职责是把不同厂商的 API 归一成统一的消息输入与结构化输出(文本、工具调用、流式增量)。框架不应假设某个模型的特有能力,而应通过能力描述(capability)让上层按需使用。
消息(Message) 是框架内所有交互的统一数据模型,包括角色、内容、工具调用、工具结果、元数据。消息结构一旦稳定,日志、回放、评测、训练数据导出都建立在它之上。
工具(Tool) 是能力暴露的标准契约:名称、描述、参数 Schema、执行函数、结果结构。工具契约的质量直接决定模型能否正确调用——描述越清晰、Schema 越严格、返回越结构化,可靠性越高。
状态(State) 是执行过程中的显式上下文:消息历史、步数、预算、中间变量、任务进度。状态必须显式化、可序列化,这是持久化、恢复、调试和 human-in-the-loop 的前提。
循环(Runner / Loop) 是驱动"决策—执行—观察"的核心引擎。它负责推进状态、调用模型、分发工具、判断终止。循环是框架的心脏,也是最需要精心设计的部分。
钩子/中间件(Middleware) 是横切关注点的挂载点:日志、追踪、限流、内容安全、成本统计、审批拦截。把这类能力做成中间件,而不是写进循环,是框架可扩展性的关键。
这六个概念构成框架的内核。多 Agent 编排、RAG、工作流,都应建立在这六者之上,而不是另起炉灶。
一个可长期维护的 Agent 框架,通常分为五层,依赖方向由外向内。
内核层 只包含消息、工具契约、状态、循环和中间件机制。它应该尽量小、尽量稳定,不依赖具体模型厂商,不依赖具体存储。内核越小,演进越安全。
能力层 包括模型适配器、工具实现(搜索、代码执行、HTTP)、记忆后端(向量库、数据库)。这一层是"可替换"的,框架应定义接口而非绑定实现。
编排层 负责多 Agent、工作流、条件分支、并行执行。它建立在内核原语之上,属于"可选组件",不应污染内核。
运行时层 负责执行调度、并发控制、持久化、检查点与恢复、超时与预算。它决定框架能否用于生产,而不是只能跑 demo。
观测层 负责追踪、指标、日志、回放。它应该内建,而不是事后补。
分层的关键判断标准是:替换模型厂商、替换存储、去掉多 Agent 能力,内核是否仍然可用? 如果内核依赖了外围实现,分层就是假的。
循环有两种常见形态:事件驱动的响应式循环,和步骤化的状态推进循环。对 Agent 而言,步骤化(step-based)通常更合适:每一步产出明确的状态变更,便于记录、暂停、恢复、评测和 human-in-the-loop。事件驱动适合 UI 交互,但会让执行语义变得难以推理。
循环必须内建三样东西:终止条件(任务完成、达到最大步数、超出预算)、状态推进的原子性(每步要么完整推进,要么可回滚)、失败处理策略(重试、降级、中断)。
状态不能藏在闭包、线程本地变量或对象字段里。它应该是一个显式的数据结构,能够序列化成快照(checkpoint)。这样带来三个能力:进程崩溃后可恢复、长任务可暂停等待人工审批、每一步都可回放用于调试。
检查点的粒度需要权衡:每步都存成本高,存得太稀恢复时丢进度。常见做法是"关键节点存 + 可配置步数存"。
工具调用的可靠性高度依赖契约设计。参数必须用 JSON Schema 严格约束,框架在执行前校验;执行结果应统一包装成"成功/失败 + 数据 + 错误信息"的结构;工具失败不应直接终止循环,而应作为观察结果回填给模型,让模型有机会换参数重试或换工具。这是 Agent 鲁棒性的关键设计。
长任务会导致消息历史无限增长,超出模型上下文窗口。框架需要提供上下文管理策略:按重要性截断、对历史做摘要、对工具结果做压缩、保留关键锚点消息。这应是可插拔策略,而不是写死在循环里。
生产级 Agent 经常需要"执行到某一步暂停,等人审批后继续"。这要求循环支持中断点、状态可持久化、恢复时能从检查点继续。把 human-in-the-loop 当补丁做,最终会变成一堆难以维护的特殊分支;把它设计成状态机的一个正常状态,架构会干净得多。
每一步的模型输入输出、工具调用、耗时、token 消耗、决策理由,都应以结构化形式记录,并支持关联到同一次任务追踪(trace)。没有这些,框架使用者无法定位失败原因,框架维护者也无法改进。
框架的稳定性首先体现在契约上。下面两个接口定义了框架与使用者的边界。
from typing import Protocol, Any
from dataclasses import dataclass, field
import json
class Tool(Protocol):
name: str
description: str
parameters: dict # JSON Schema,执行前必须校验
def execute(self, args: dict[str, Any]) -> "ToolResult": ...
@dataclass
class ToolResult:
ok: bool
data: Any = None
error: str | None = None # 失败也要结构化回填给模型
@dataclass
class AgentState:
messages: list[dict] = field(default_factory=list)
step: int = 0
max_steps: int = 20
metadata: dict = field(default_factory=dict)
def snapshot(self) -> str: # 可序列化 = 可持久化/可恢复
return json.dumps(self.__dict__, ensure_ascii=False)
@property
def done(self) -> bool:
return self.step >= self.max_steps要点:工具以契约而非基类定义,使用者可自由实现;错误是一等公民,结构化回填;状态显式且可序列化,为检查点与恢复打基础。
把横切关注点从循环里剥离,是框架可扩展性的核心。中间件以"环绕"方式包裹每一步,循环本身保持极简。
from contextlib import asynccontextmanager
def compose(middlewares, core):
for mw in reversed(middlewares):
core = mw(core)
return core
def logging_mw(next_step):
async def _step(state, ctx):
ctx.trace.span("agent.step", step=state.step)
try:
return await next_step(state, ctx)
finally:
ctx.trace.end("agent.step")
return _step
async def run(state: AgentState, ctx) -> AgentState:
async def core(state, ctx):
decision = await ctx.model.decide(state) # 模型决策
state.messages.append(decision.to_message())
if decision.is_final:
state.max_steps = state.step # 终止
else:
for call in decision.tool_calls: # 分发工具
result = await ctx.tools.execute(call) # 失败也返回 ToolResult
state.messages.append(result.to_message())
state.step += 1
return state
step = compose([logging_mw, *ctx.middlewares], core)
while not state.done:
state = await step(state, ctx)
await ctx.checkpoint.save(state) # 每步可恢复
return state要点:循环只做"决策—执行—推进";日志、限流、审批、成本统计都通过中间件挂载;每步保存检查点,支持崩溃恢复和人工中断后继续。
工具失败不能让整个 Agent 崩掉。统一的执行包装把异常转成结构化结果,回填给模型
import jsonschema
async def execute(self, call) -> ToolResult:
tool = self._registry.get(call.name)
if tool is None:
return ToolResult(ok=False, error=f"unknown tool: {call.name}")
try:
jsonschema.validate(call.args, tool.parameters) # 参数强校验
data = await tool.execute(call.args)
return ToolResult(ok=True, data=data)
except jsonschema.ValidationError as e:
return ToolResult(ok=False, error=f"invalid args: {e.message}")
except Exception as e:
return ToolResult(ok=False, error=f"tool failed: {e!r}") # 结构化回填要点:未知工具、参数错误、执行异常都被转换为 ToolResult,模型可以看到失败原因并调整策略;框架不因单次工具失败而中断整个任务。
第一,把业务策略写进内核。 例如把某种特定的提示词模板、某种固定的终止逻辑、某个厂商的特有能力做成内核的一部分。内核应该只提供原语,策略由使用者组合。
第二,状态藏在对象里。 用闭包、单例、线程本地变量保存执行上下文,导致无法持久化、无法恢复、无法并行。状态必须显式传递。
第三,工具失败即崩溃。 工具异常直接抛出终止循环,是 Agent 不可靠的典型原因。错误应该被结构化回填,让模型有机会自我修正。
第四,可观测性后补。 先跑通再想日志,最终会发现关键信息没记录、追踪无法关联、失败无法归因。可观测性必须在内核设计时就纳入。
第五,过早抽象多 Agent。 在内核还不稳定时就设计复杂的协作拓扑,会让框架迅速臃肿。多 Agent 应建立在稳定的单 Agent 原语之上,作为可选编排层。
第六,忽略上下文膨胀。 不提供截断、摘要、压缩策略,长任务必然在某个时刻爆上下文窗口。上下文管理应是框架的标配能力。
务实的框架演进顺序是:先稳定消息、工具、状态三者的契约;再实现最小可用的步骤化循环与错误回填;然后加入中间件机制与内建可观测性;接着实现检查点持久化与中断恢复;最后,在单 Agent 内核稳定之后,再构建多 Agent 编排与工作流。
检验框架设计是否成功的标准很朴素:换一个模型厂商、换一种存储、去掉多 Agent 能力,内核是否仍然稳定可用?使用者能否在不修改内核的前提下表达新的业务策略? 两个答案都是肯定的,框架的抽象就站得住。
Agent 框架设计的本质,不是把功能堆全,而是在快速变化的领域里找到稳定的抽象边界:内核只保留消息、工具、状态、循环、中间件这些不变的概念,把模型、存储、编排、策略都推到可替换的外围。框架的价值不在于替你写完业务,而在于让你在写业务时不必重复造轮子,也不必被框架绑死。当你的框架能做到"内核小、契约稳、外围可换、失败可恢复、行为可观测"时,它才真正具备被长期使用和持续演进的能力。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。