首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent 框架设计实战指南:从核心抽象到可恢复执行的架构决策

Agent 框架设计实战指南:从核心抽象到可恢复执行的架构决策

原创
作者头像
资源shanxueit.com
发布于 2026-10-28 14:08:52
发布于 2026-10-28 14:08:52
480
举报

一、为什么框架设计比用框架更难

用现成的 Agent 框架搭一个 demo 很容易:定义几个工具、塞一段提示词、跑一个循环,半小时就能看到效果。但当你要设计一个框架本身——让它能被多个团队、多种业务、多类模型复用——难度会陡然上升。

原因在于,框架设计者必须回答一组应用开发者不必回答的问题:哪些概念应该成为一等公民,哪些应该留给使用者自由组合?状态该放在哪里,如何持久化和恢复?工具调用失败算谁的责任?循环终止条件由框架定还是由业务定?可观测性应该是可选项还是内建能力? 每一个问题背后都是契约的取舍,而契约一旦发布,改起来成本极高。

更棘手的是,Agent 领域本身仍在快速演进:模型能力在变、工具调用协议在变、推理范式在变。一个好的框架不能把当下某一种范式写死,而要在变化中保持稳定的抽象。这就是本文要讨论的核心: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 鲁棒性的关键设计。

上下文管理:截断、摘要与压缩

长任务会导致消息历史无限增长,超出模型上下文窗口。框架需要提供上下文管理策略:按重要性截断、对历史做摘要、对工具结果做压缩、保留关键锚点消息。这应是可插拔策略,而不是写死在循环里。

中断与恢复:human-in-the-loop 是一等公民

生产级 Agent 经常需要"执行到某一步暂停,等人审批后继续"。这要求循环支持中断点、状态可持久化、恢复时能从检查点继续。把 human-in-the-loop 当补丁做,最终会变成一堆难以维护的特殊分支;把它设计成状态机的一个正常状态,架构会干净得多。

可观测性:内建而非外挂

每一步的模型输入输出、工具调用、耗时、token 消耗、决策理由,都应以结构化形式记录,并支持关联到同一次任务追踪(trace)。没有这些,框架使用者无法定位失败原因,框架维护者也无法改进。

六、少量代码:三个体现设计取舍的片段

片段一:核心契约——工具与状态

框架的稳定性首先体现在契约上。下面两个接口定义了框架与使用者的边界。

代码语言:javascript
复制
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

要点:工具以契约而非基类定义,使用者可自由实现;错误是一等公民,结构化回填;状态显式且可序列化,为检查点与恢复打基础。

片段二:可组合的中间件与主循环

把横切关注点从循环里剥离,是框架可扩展性的核心。中间件以"环绕"方式包裹每一步,循环本身保持极简。

代码语言:javascript
复制
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 崩掉。统一的执行包装把异常转成结构化结果,回填给模型

代码语言:javascript
复制
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 删除。

目录
  • 一、为什么框架设计比用框架更难
  • 二、框架的第一性问题:抽象什么,不抽象什么
  • 三、核心抽象:Agent 框架的六件套
  • 四、分层架构:内核要小,外围要可换
  • 五、关键机制设计
    • 执行循环:步骤化优于事件化
    • 状态管理:显式、可序列化、可恢复
    • 工具调用:契约严格,错误可反馈
    • 上下文管理:截断、摘要与压缩
    • 中断与恢复:human-in-the-loop 是一等公民
    • 可观测性:内建而非外挂
  • 六、少量代码:三个体现设计取舍的片段
    • 片段一:核心契约——工具与状态
    • 片段二:可组合的中间件与主循环
    • 片段三:工具执行的防御式包装
  • 七、常见设计陷阱
  • 八、演进路径建议
  • 九、结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档