市面上多数 Agent 框架的宣传点是"编排"——把节点连成图、把多个 Agent 串起来、拖拽出工作流。但对真正要上线的人来说,这些不是难点。难点在于:一个不可靠的模型,被放进一个可能失败的循环里,如何让它稳定地、可恢复地、成本可控地跑完。
把这个问题拆开,Agent 框架的核心职责只有三块:
职责 | 本质问题 | 常见误判 |
|---|---|---|
循环控制 | 什么时候继续、什么时候停、什么时候该问人 | 以为这是编排问题 |
上下文管理 | 有限窗口里放什么、丢什么、怎么压缩 | 最被忽视,也是效果差距的主因 |
工具运行时 | schema、执行、错误语义、权限、幂等 | 以为只是注册函数 |
一个框架如果只做好了编排而没做好这三块,做出来的东西就是"能演示、不能上线"。下面逐块展开,并在关键处给出设计取舍和少量代码。
这是设计 Agent 框架的第一个、也是最根本的决策。
厚框架(图 DSL、节点、边、状态对象、检查点原语、多 Agent 原语)的好处是开箱即用,坏处是抽象泄漏时你无处可逃。Agent 的控制流本质上是"模型输出决定的动态分支",把它硬塞进静态图,一旦模型行为超出图能表达的范围,你就要开始绕框架的弯。调试时你会对着框架内部的 trace 猜测"到底哪一步出了问题"。
薄框架(只提供循环骨架、上下文管理、工具运行时,控制流由调用方用普通代码写)的代价是自己写更多胶水,好处是所有控制流都是你读得懂的代码,出问题直接断点调试。
专业建议是:默认选薄的,只在确证需要时加厚。 具体地说,框架应该提供的是"可组合的原语",而不是"规定了写法的框架":
run_loop(model, tools, state, limits) 的执行器把"流程长什么样"交给调用方。这样做的另一个好处是:工作流和 Agent 可以用同一套原语表达——确定性的步骤直接写顺序代码,不确定的地方调用循环原语。生产系统绝大多数是这种混合形态,而不是纯 Agent。
三种执行模型对应三种任务确定性:
模型 | 适用 | 优点 | 代价 |
|---|---|---|---|
自由循环 | 路径不可预知(研究、排障、开放式任务) | 表达力最强 | 不可预测、易死循环、难评估 |
状态机 | 阶段明确、可枚举(审批流、多轮表单) | 可验证、可测试、可恢复 | 表达力受限 |
图/工作流 | 步骤确定、依赖明确(ETL、编排) | 可观测、可并行 | 不支持动态分支 |
生产实践里最常见的错误是用自由循环去做本该状态机化的任务。 一个"先收集信息、再校验、再提交"的流程,如果写成 Agent 自由发挥,你会得到:偶尔跳过校验、偶尔重复提交、无法保证顺序。这类任务应该显式建模为状态,循环只用于单个状态内部的决策。
反过来,把开放式排障任务塞进状态机,就会不断遇到"这个分支图里没有"的情况。
判断标准很简单:问一句"如果模型这一步做错了,业务上能不能接受?" 不能接受的部分必须有显式结构,能接受的部分才交给循环。
Agent 效果的上限,一大半由上下文管理决定。而多数框架对这一层几乎没有设计,只是把 messages 列表一直往后 append。结果是:跑到第 15 步上下文爆了,或者关键信息被埋在几万 token 之前。
上下文管理需要三个明确的机制:
1. 预算分配。 给每一类内容划出 token 预算:系统提示、工具定义、历史、检索结果、当前观察。总预算要留出输出空间。没有预算意识,等于把窗口当成无限。
2. 分层压缩。 不是"超了就截断最后 N 条",那会丢掉最早的任务定义。合理的顺序是:
3. 外部化。 大对象不放上下文,放外部存储,上下文里只留引用和摘要,需要时再取。这是子 Agent 隔离和长任务能跑下去的底层手法。
一段最小但完整的上下文构建示意:
def build_context(state, budget_tokens=120_000):
# 1) 不可压缩的核心:任务定义与约束永远保留
head = [state.system_prompt, state.task_definition]
# 2) 近期消息原样保留(最近几轮信息密度最高)
recent = state.messages[-state.keep_recent:]
# 3) 更早的历史折叠为摘要
older = state.messages[:-state.keep_recent]
summary = state.running_summary # 增量维护,而非每次重算
if older:
summary = summarize_incremental(older, prev=summary)
# 4) 工具结果按类型裁剪:大结果只留引用
for m in recent:
if m.role == "tool" and len(m.content) > 2000:
ref = state.store.put(m.content) # 外部化
m.content = f"[已存为 {ref},摘要: {head_of(m.content)}]"
ctx = head + ([{"role": "system", "content": summary}] if summary else []) + recent
return enforce_budget(ctx, budget_tokens) # 按优先级裁剪,触底则中止并上报要点在最后一行:预算兜底时要显式中止或降级,而不是静默截断。静默截断导致的"模型突然忘了任务",是最难排查的一类问题。
工具设计最常见的三个错误:
1. 描述写给人类看。 工具的 description 和参数说明是给模型看的提示词,不是 API 文档。要说清"什么时候该用""什么时候不该用""参数含义和常见取值",而不是复述函数签名。
2. 错误抛异常。 工具失败(参数错、超时、权限不足、下游 500)必须作为结果回填给模型,并附带可操作的错误信息,让模型自己决定重试、换参数还是换路径。直接抛异常会终止整个循环,模型失去了纠错机会。
3. 没有幂等和权限。 模型会重试,会重复调用。所有有副作用的工具(下单、发邮件、写库)必须支持幂等键;所有危险工具必须走独立的权限校验,不能依赖提示词约束。
def execute_tool(call, registry, ctx) -> dict:
spec = registry.get(call.name)
if spec is None:
return {"ok": False, "error": f"未知工具 {call.name}", "hint": "可用工具见工具列表"}
# 权限与前置条件:框架层强制,不交给模型判断
if not ctx.policy.allow(call.name, call.arguments):
return {"ok": False, "error": "无权限执行该操作", "hint": "需要用户确认或提升权限"}
try:
args = spec.validate(call.arguments) # schema 校验在框架侧做
result = spec.fn(**args, _idem_key=call.id) # 幂等键由框架统一注入
return {"ok": True, "data": spec.truncate(result)}
except SpecError as e:
return {"ok": False, "error": str(e), "hint": "参数不符合要求,请参考 schema 修正"}
except TransientError as e:
return {"ok": False, "error": str(e), "retryable": True}
except Exception as e:
log.exception("tool crashed")
return {"ok": False, "error": "内部错误", "retryable": False}所有错误都以结构化结果返回,循环永不因工具异常中断。 这一条决定了 Agent 的鲁棒性下限。
Demo 级 Agent 和上线级 Agent 的差别,几乎全在这一节。
一个跑 30 步、涉及 15 次工具调用、耗时几分钟的 Agent,中间可能遇到:进程重启、超时、下游限流、模型返回异常。如果状态只在内存里,任何一次故障都意味着从头重来——重复的副作用、重复的花费、不可接受的延迟。
解法是持久化执行(durable execution):把循环的每一步作为一个可重放的事件记录,每次状态变更后落 checkpoint,恢复时从最后一个 checkpoint 重放,已完成的步骤直接跳过。
def run_durable(agent_id, state_store, executor):
state = state_store.load(agent_id) or executor.init_state()
while not state.done and state.step < state.max_steps:
# 已完成的步骤重放时直接跳过(幂等保证副作用不重复)
if state.step in state.completed_steps:
state.step += 1
continue
action = executor.decide(state) # 模型决策
state.begin_step(state.step, action)
state_store.save(agent_id, state) # 先记意图
result = executor.act(action, state) # 执行(含幂等键)
state.finish_step(state.step, result)
state_store.save(agent_id, state) # 再记结果
state.step += 1
return state_store.finalize(agent_id, state)关键设计点:
这一层做不做,决定了 Agent 是玩具还是系统。
多 Agent 最容易走进的误区,是以为"拆成多个 Agent 就更聪明"。实际上,多 Agent 唯一可靠的收益是上下文隔离——每个子 Agent 有自己的窗口,只返回摘要给主 Agent,避免主上下文被过程细节撑爆。
它能做到的:并行探索多条路径、隔离不同领域的工具集与提示词、把长过程压缩成结论。
它做不到的:真正提升单步推理质量。把任务拆给两个 Agent,不会让任何一个变得更聪明;如果拆分引入了信息损失(子 Agent 看不到主 Agent 的某些约束),整体还会变差。
所以设计原则是:
Agent 的调试难度远高于普通程序,因为失败往往是概率性的、多步累积的。可观测性需要覆盖到决策依据,而不只是调用日志:
评估上,要区分过程指标和结果指标:工具调用正确率、无效循环次数、上下文压缩是否丢信息,属于过程;任务完成率、人工接管率、单任务成本,属于结果。只看结果无法定位问题,只看过程无法判断价值。
一个实用做法是把每次线上失败样例固化成回归用例,Agent 的评估集是长出来的,不是一开始就设计好的。
给正在做或准备做 Agent 框架的人:
Agent 框架的专业性,不体现在支持多少种编排方式,而体现在它是否认真对待了"模型不可靠、循环会失败、上下文会溢出、进程会重启"这四个现实。把这四件事处理好的框架,哪怕只有几百行,也比功能齐全但回避它们的框架更值得用在生产里。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。