从让模型少想一点,到让 Runtime 少管一点:重新理解 Agent、Runtime、MCP 与 Skill 的分工

从让模型少想一点,到让 Runtime 少管一点:重新理解 Agent、Runtime、MCP 与 Skill 的分工

做一个严肃一点的 Agent,很容易经历两个阶段。

第一阶段,发现模型不稳定,于是不断把状态、校验、权限、完成条件往 Runtime 下沉。第二阶段,Runtime 越来越可靠,也越来越重,最后开始顺手拥有工具定义、业务分类、数据投影和调用语义。

我们这个投资研究 Agent 最近一段时间就在经历这样的变化。

最早的问题是:哪些事情不该让模型维护?

后来问题变成了:哪些事情虽然不该让模型维护,也不应该归 Runtime?

截至 2026-09-30,这段演进已经形成了一条比较清晰的主线:

Model 负责语义,Runtime 负责过程,MCP 负责能力,Skill 负责方法。

这不是一开始画出来的架构图,而是从几次真实重构里慢慢收敛出来的。


一、第一步:先别让模型维护 Runtime 已经知道的事实

早期 Research 协议里,我们让模型输出一个结构化的 AgentTurn。它不仅表达下一步动作,还会携带状态、缺口、完成声明等信息。

简化后大概是:

json 复制代码
{
  "actions": ["search"],
  "gaps": ["还缺来源"],
  "completionClaim": false
}

这个设计一开始并不奇怪。工具调用需要审计,Research 需要恢复,系统也希望知道模型为什么继续、为什么结束。

问题是协议会自然膨胀。

当权限、工具集合、证据缺口、发布状态、子目标、完成条件都逐渐进入模型输出后,模型实际上同时做两件事:

text 复制代码
理解问题、判断下一步
+
维护一份 Runtime 状态镜像

第二件事很贵,而且通常没有增加任何权威。

Runtime 本来就知道:

  • 某项能力是否真的获批;
  • 某个 Tool 是否执行;
  • 结果是否成功返回;
  • publication 是否提交;
  • 当前 run 是否取消;
  • 还剩多少预算。

如果模型再输出一份,这份状态最多是"模型认为现在是什么状态"。一旦冲突,系统仍然只能相信 Host 实际记录的事实。

所以我们后来把 Research 收敛到 native tool loop。模型不再描述完整工作流状态,而是提出下一步 Tool 调用;Host 持久化 ModelTurn、执行结果、receipt 和 publication,Runtime 用这些事实控制恢复和终态。

2026-09-20 的历史记录里,这个边界被概括成一句话:

语义决策可以交给模型,权威与恢复不能。

这一步的核心不是减少系统状态,恰恰相反,系统状态反而需要更严格。减少的是模型对系统事实的重复维护。


二、把复杂度下沉到 Runtime 是对的,但下沉不是越多越好

从 AgentTurn 迁到 native tool calling 以后,一个很自然的认识是:

Runtime 已经能确定的事情,就不要再让模型推理。

于是越来越多确定性逻辑进入 Runtime:

text 复制代码
授权
预算
调用状态
取消
checkpoint
recovery
receipt
publication
terminal gate

这些职责有一个共同特点:答案应该是确定的。

例如:

text 复制代码
remainingReadActions = 3

不需要模型判断。

某个 receipt 是否已经 durable,也不需要模型判断。

但"确定性逻辑应该下沉"很容易被扩展成另一句话:

只要不是模型负责,就放 Runtime。

这两句话并不等价。

我们后来真正踩到的问题,就是 Runtime 开始拥有越来越多业务能力本身的定义。


三、第一次 Tool 收敛:从多份定义变成一个 Tool Contract

2026-09-26,我们先处理了一类很典型的 drift:同一个 Tool 的能力关系在多个位置重复定义。

当时 Tool Contract 已经存在,但 production Runtime 还维护一份 capability table。新增或调整一条 Host route 时,两个地方都要保持一致。

这次改造的方向很直接:

text 复制代码
做前:
Host catalog
+
Runtime capability table

做后:
Host catalog
→ Runtime 查询

聚焦合同、Host 和生产路径的回归一共跑了 162 个用例;后续整合离线集记录为 438 个文件、3,915 个测试通过、1 个跳过。这里证明的是重复 capability lookup 被移除以及相关 provider-free 行为没有回退,不代表模型质量或正式验收。

第二天我们又发现,重复还不止两份。

Native Tool、Skill 和 Host 的关系分别存在于:

text 复制代码
Research Tool Contract
Research Skill Context
Research Runtime Factory

比如一个 Skill 允许哪个 MCP Tool,会进一步映射成哪个 Native Model Tool、哪个 Host Action、哪种 capability。Web search 和 read 又共享 Host route,单靠 route 很容易把 sibling Tool 一起暴露出来。

于是 2026-09-27 又做了一次收敛:

text 复制代码
三份静态 owner
↓
一个 Tool Contract
↓
派生 Skill / Host / capability exposure

这次历史记录里留下了一句很漂亮的总结:

三 owner → 一 owner。

当时这个判断是正确的。重复定义确实减少了,search-only / browse-only Skill 的暴露也能更精确地控制。

但后来的问题说明:单一 owner 只是第一层问题。


四、真正的问题:剩下的这个 owner,放对地方了吗?

最近我们重新看 Portfolio 工具时,发现了一个很明显的信号。

Portfolio MCP 本身已经暴露了多种 scoped capability,例如:

text 复制代码
portfolio__get_portfolio_overview
portfolio__get_fund_position
portfolio__get_bucket_holdings
portfolio__get_sector_exposure

这些名字、参数和返回语义都很明确。

但是 Native Research 没有直接把这些 capability 给模型,而是又定义了一层 synthetic Tool:

text 复制代码
read_portfolio({})

调用链因此变成:

text 复制代码
MCP Tool
  ↓
Research Tool Contract
  ↓
Model Tool
  ↓
Host Action
  ↓
mapReadCall
  ↓
MCP Tool

每一层单独看都能解释:

  • Runtime 要管权限;
  • 要管 Skill ceiling;
  • 要管预算;
  • 要管输入;
  • 要管输出大小。

但这些治理需求并不能推出一个结论:

Runtime 应该重新定义一次业务 Tool。

Portfolio 场景把问题暴露得很直观:MCP 已经有 bucket holdings,模型却只看到一个宽泛的 read_portfolio。Runtime 为了治理 Tool,顺手成为了 Tool 语义的 owner,结果反而把上游已经表达清楚的能力压扁了。

这时候我们重新做 deletion test:

如果删掉 synthetic Tool layer,复杂度会扩散到调用方,还是直接消失?

对 MCP-backed business tools 来说,大量消失的是:

text 复制代码
重复的名字
重复的 schema
重复的 mapping
重复的 Host Action identity

而真正重要的治理并没有消失:

text 复制代码
authorization
budget
hook
recovery
receipt
result bound

这说明我们之前完成的是:

text 复制代码
3 owners → 1 owner

但现在真正要做的是:

text 复制代码
错误位置的 1 owner → 0 个 shadow owner

截至本文写作时,agent-remove-synthetic-mcp-tool-layer 已经把这套目标写成 OpenSpec,但 implementation 还没有在本文证据窗口里完成,所以后文涉及这一层时都按目标架构描述。


五、目标分工:模型 / 技能 / MCP / 运行时

这次重新划分后,我更愿意把四层理解成四种不同的复杂度。

flowchart TD U["用户问题"] --> M1["模型:理解与选择"] S["技能:提供方法"] -.-> M1 M1 --> T["工具面:本轮可见能力"] T --> P1["调用前钩子:机械治理"] P1 --> C["MCP:能力接口"] C --> P2["调用后钩子:结果收窄"] P2 --> M2["模型:继续推理"] R["运行时:状态与生命周期"] -.-> T R -.-> P1 R -.-> P2

这张图是正在推进的职责目标,不表示所有链路已经完成迁移。

模型:负责语义复杂度

模型适合回答:

text 复制代码
用户到底想解决什么?
还缺什么信息?
下一步该查哪一类资料?
两个证据冲突时应该继续查哪边?
最后该怎么解释?

这些问题没有一个固定规则能在所有上下文下得到唯一答案。

技能:负责方法复杂度

Skill 更像一套可复用的方法,而不是数据接口。

例如"分析我的基金是不是重复"这个任务,Skill 可以规定:

text 复制代码
先看整体组合
→ 再看集中度
→ 对候选基金读取关系证据
→ 区分同产品、同指数和底层持仓重叠
→ 回答时带日期和覆盖范围

Skill 可以声明 allowed-tools、required-tool-attempts 或回答结构,但它不应该自己下载持仓、管理 sourceRef、缓存页面或维护恢复状态。

一句话:

Skill 说明这类问题怎么做。

MCP:负责能力复杂度

MCP 回答的是:

系统能做什么?

一个业务 Tool 的 identity、description、input schema 和 output semantics,天然应该由提供这个 capability 的 owner 定义。

例如:

text 复制代码
portfolio__get_bucket_holdings

本身就已经表达一个完整能力:读取某个 Portfolio bucket 的持仓。

它不应该先变成一个更模糊的 Runtime Tool,再被映射回来。

所以我现在更愿意把 MCP 理解成:

capability interface,而不只是 data connector。

运行时:负责过程复杂度

Runtime 负责的是一次 run 怎么活着走完:

text 复制代码
当前 phase
预算与消耗
取消
checkpoint
recovery
receipt
continuation
publication orchestration
terminal condition

Runtime 可以提供:

text 复制代码
remainingReadActions = 3
phase = finalization-only

但它不应该因为自己知道这些状态,就顺手定义 Portfolio、Market、Web 各自有哪些业务 Tool。


六、一个具体例子:同一句持仓问题,四层分别做什么

假设用户问:

"红利低波这一块我到底买了哪些基金?是不是重复得太多?"

下面是教学示例,使用合成数据,不对应任何真实持仓,也不是现行 wire contract。

模型首先做语义判断:

json 复制代码
{
  "goal": "检查红利低波桶的组成与重复关系",
  "nextStep": "读取该 bucket 的持仓,再按需要读取关系证据"
}

Skill 提供方法约束:

text 复制代码
不要仅凭基金名字判断重复;
需要区分产品关系、同指数和 measured constituent overlap;
部分穿透不能写成全量重叠率。

MCP 提供能力:

text 复制代码
portfolio__get_bucket_holdings
portfolio__get_portfolio_relationships

一次 bucket read 的结构化结果可以类似:

json 复制代码
{
  "bucket": "dividend_low_volatility",
  "items": [
    {
      "fundCode": "F001",
      "fundName": "示例基金 A",
      "value": 10000,
      "weight": 0.08
    },
    {
      "fundCode": "F002",
      "fundName": "示例基金 B",
      "value": 7000,
      "weight": 0.056
    }
  ],
  "coverage": {
    "requested": 2,
    "returned": 2,
    "complete": true
  }
}

Runtime 不需要知道"红利低波是不是重复太多"。

它只需要知道:

text 复制代码
这项 Portfolio capability 是否已经授权?
当前 Tool 是否可执行?
调用是否完成?
结果是否在大小边界内?
receipt 是否已经落库?

这样每一层都只承担自己真正拥有的复杂度。


七、Hook 的边界:治理调用,不重新实现业务语义

把 Tool 语义还给 MCP,并不意味着 Tool 调用不再治理。

PreToolUse / PostToolUse 很适合做跨层的机械治理。

PreToolUse:只做确定性的单字段处理

适合:

text 复制代码
2026/9/30 → 2026-09-30
1万 → 10000
确定市场后的证券代码规范化
人工审核过的一对一枚举 alias

这些处理有共同特点:

  • 输入已经位于明确字段;
  • 映射唯一;
  • 不需要理解整句话;
  • 可以稳定测试。

不适合:

text 复制代码
"腾讯最近跌得很多"
→ 猜测用户其实想查 0700.HK 并分析是否减仓

这已经是语义理解,应留给 Model。

PostToolUse:可以收窄,不要重新造业务模型

适合:

text 复制代码
删除敏感字段
减少模型不需要的内容
做最终大小检查
保留 coverage / provenance

如果一个 Tool 每次返回后都需要在 PostHook 里重新分类、重新聚合、重新分页、重新构造业务语义,通常说明 Tool Interface 设计得不对。

更合理的修复是:

回到 MCP,增加一个真正适合该场景的 view。

而不是让 Runtime 或 Hook 长成第二套业务层。


八、三个对比案例:为什么"更集中"不一定等于"更正确"

案例一:AgentTurn → native tool loop

旧做法:

text 复制代码
模型维护 actions / gaps / completionClaim / subGoals

后来的做法:

text 复制代码
模型提出 Tool call
Runtime / Host 维护真实执行事实

这里的收益不是"JSON 字段更少"本身,而是状态 owner 回到了真正拥有事实的地方。

案例二:三份 Tool mapping → 一份 Tool Contract

旧做法:

text 复制代码
Tool Contract
Skill Context
Runtime Factory

分别维护同一个 Tool 的暴露关系。

2026-09-27 收敛成一个 Tool Contract,是必要的一步,因为它减少了 drift。

但这次重新 review 又发现:

一个 owner 仍然可能是错误 owner。

如果 MCP 已经定义完整业务 capability,那么 Runtime Tool Contract 再拥有一遍业务 Tool Interface,仍然是 shadow owner。

案例三:测试全绿,但 production 已经不经过这个 seam

这次继续扫仓库时,我们还发现了另一类历史债务:部分 src/server Module 已经没有 production consumer,但测试仍然直接调用它们。

典型结构是:

text 复制代码
Production:
A → B → C

Tests:
T → LegacyModule

测试绿,只能证明 LegacyModule 自己的 Interface 仍然符合测试,并不能证明 production 还经过这里。

因此我们又开了独立的 agent-retire-test-only-and-legacy-seams,准备把仍有效的 invariant 迁回真实 production seam,再删除旧 Module。这个 change 在本文证据窗口里同样只完成了 authoring,不能写成已经清理完成。

这个案例让我重新理解了一句话:

The interface is the test surface。

如果 production seam 已经移动,测试也要跟着移动。否则测试会开始保护历史结构,而不是保护系统行为。


九、失败与降级边界:分工变清楚,不代表边界变软

把职责拆开,并不是把所有逻辑交给模型。

恰恰相反,有些硬边界必须继续由确定性代码执行。

私有 Tool 在授权前不可见

目标设计里,Portfolio 这类私有 Tool 在 capability grant 前不进入本轮 Tool Surface;授权成功后,下一 ModelTurn 才能看到。

即使可见,PreToolUse 仍然要重新检查本次调用是否允许。

可见性不是授权。

机械 canonicalization 有歧义就停止

日期、金额、证券代码或精确 alias 可以规范化,但只要需要理解句子或存在多个合理映射,就不猜。

返回结构化纠正信息,让模型重新决定。

结果太大就显式不可用

不能为了让模型"至少拿到一点"就把一个声称 complete 的持仓列表静默截成 Top N。

complete-or-unavailable 比"看起来成功"更重要。

Web source state 不能为了拆 wrapper 而丢失

如果当前 Web wrapper 承担 run-scoped source handle、read-before-find、revocation 等真实 invariant,就必须先把这些责任迁回 Web MCP / Gateway,再删除 wrapper。

删除中转层的目标是消灭重复 owner,不是消灭安全边界。


十、现在我会用五个问题判断逻辑应该放在哪里

以后再看到一段 Agent 逻辑,我会先问:

1. 这件事需要理解意义吗?

是 → Model

例如用户意图、下一步调查、证据冲突、解释。

2. 这件事是在描述"这类任务怎么做"吗?

是 → Skill

例如基金重叠分析需要哪些步骤、需要尝试哪些能力、回答包含哪些部分。

3. 这件事定义的是"系统能做什么"吗?

是 → MCP / capability owner

例如读取组合、查询基金、浏览页面、获取指数成分。

4. 这件事是运行过程中可确定的状态吗?

是 → Runtime

例如预算、取消、receipt、checkpoint、recovery、terminal。

5. 如果删掉这一层,复杂度到底去哪了?

  • 复杂度扩散给多个调用方:这层可能有价值;
  • 复杂度被一个更深的 owner 吸收:应该 deepening;
  • 只有 mapping、schema copy、alias 和自测消失:大概率应该删。

第五个问题尤其重要。

我们之前已经做对了"多 owner → 单 owner",但这次才真正补上:

单 owner 之后,还要继续问 owner 是否放对了地方。


最后:复杂度不是越往下沉越好,而是要放到正确 owner

我们最早的经验是:

运行时已经知道的,不要让模型再维护。

这条原则帮我们从重型 AgentTurn 走到了 native tool loop。

但继续演进之后,还需要再加一句:

Runtime 能执行,不代表 Runtime 就应该拥有这个业务概念。

所以现在我更愿意把整个 Agent 系统理解成四种复杂度:

text 复制代码
Model   → 语义复杂度
Skill   → 方法复杂度
MCP     → 能力复杂度
Runtime → 过程复杂度

钩子负责它们之间的机械治理,工具面负责本轮能力可见性的投影。

好的 Agent 架构不是把复杂度消灭掉,也不是把复杂度全部赶进 Runtime。

真正重要的是:

谁拥有事实,谁维护事实;谁拥有能力,谁定义 Interface;需要理解意义的事情,才交给模型。

从"让模型少想一点"走到"让 Runtime 少管一点",本质上都是同一件事:

把复杂度放回最适合承担它的地方。

相关推荐
Chengyunlai2 小时前
Agent应用开发和渲染之间关系的思考
agent
智能RPA2 小时前
能源电力行业智能体自动化平台对比评测(调度与抄表场景)
人工智能·自动化·能源·agent·rpa
沛东的认知和实践2 小时前
LLM-Wiki总论:把知识库编译成 Wiki,再把检索权交给 LLM——彻底搞懂LLM Wiki第一篇
agent
去伪存真2 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent
墨心@4 小时前
第 4 章《工具》学习总结
学习·自然语言处理·prompt·agent·harness
七夜zippoe4 小时前
WorkBuddy Agent + Blender 5.2 bpy 程序化建模实战:12 轮迭代建一座冬日微缩展示
agent·blender·建模·workbuddy·程序化实战
智能RPA5 小时前
农业与矿业行业智能体自动化平台对比评测(计量与巡检场景)
运维·人工智能·python·自动化·agent·rpa
user4465117917915 小时前
AgentScope 2.0 框架技术解析
agent
sg_knight5 小时前
ZCode Bug 定位实战:把报错丢给 ZCode,它是怎么修的
llm·bug·agent·ai编程·glm·智谱·zcode