最近一直在高强度开发中,终于得空把这段时间关于 Agent 的思考沉淀下来。

从去年 Manus 爆火到今年 Claude Code 被社区反编译公开(CC 自己是用压缩后的 npm 包发布的,社区做的是反编译/逆向,严格说不叫"源码泄露",但社区习惯性叫泄露我就跟着说),再到最近大家讨论"什么语言最适合写 Agent"——可以感觉到圈内对 Agent 的理解越来越深。CC 这事儿被动推动了全球 agent 核心研发人员至少 20% 的能力进步 :)。我也是在这个过程里,从一开始的"框架优先"思路慢慢沉淀出自己的 Agent 方法论。

我走过的路:最开始用 agno 开发但流式问题始终难以解决;后来 openclaw 爆火让 pi-mono 进入视野;前段时间研读 Claude Code 源码——看完几套顶尖架构之后,终于可以认真讨论当前的 Agent 开发范式。

我自己也按这套思路做了一个 Python 微内核,名字叫 Mauri,跑通了 3 个完全不同方向的产品迁移。这篇文章讲三件事:

  1. 沉淀出的 L1-L7 七层模型——Agent 系统的清晰分层
  2. 跨 Claude Code / pi-mono / Mauri 3 个独立实现的交叉对比——为什么 L1-L3 收敛、L4-L6 分化
  3. 不同方向产品迁移的实测数据 + 5 件收敛不到的反面证据

但在展开这三件事之前,我想先讲一个贯穿始终的核心判断——agent 的能力靠结构约束,不靠模型自觉治理 agent ≈ 设计 harness(治理层)

1. 为什么会得到这套思考

agno 有 2 层流式开关:stream=True 拿基础逐字 token 流(前端打字机 UI 够用),stream_intermediate_steps=True(新版叫 stream_events)才暴露 agentic 中间事件——ToolCallStarted / ReasoningStep 这些"模型正在思考" + 工具调用进度条要的事件。前者好解,后者是雷区——GitHub 上一串相关 issue。我修内部时序、回调透传——根因不在 stream_intermediate_steps 实现,而在 agno 核心循环本身就是"批处理 tool calls 再整批返回"的语义stream_intermediate_steps 只是事后注解,不是真正的增量事件源。这让我第一次意识到"框架的内部抽象决定上限"——一个底层"错"的抽象上面怎么样子改都是徒劳无功的

后来随着 openclaw 的爆火,pi-mono 也开始被讨论。我去读 pi-mono 的 packages/ai 才发现:流式不是接口问题,是架构问题。pi 的事件流是一组带类型标签的统一事件类型(text_start / text_delta / toolcall_start / toolcall_delta / toolcall_end / ...,每个事件都有自己的 type 字段做判别),每个模型适配器(provider adapter)必须发出同结构的事件流——agno 那种痛点在 pi 这里根本不存在。

再到 CC 源码被社区反编译公开之后我去读 query.ts,看到的是同一套结构——异步生成器主循环 + 一整组覆盖工具调用前后、压缩前后、生命周期的钩子(hook)+ 把子 agent 也包装成普通工具调用(sub-agent via tool)。两套独立来源的顶尖架构是几乎同一形态。这让我重新审视之前的判断:Agent 架构的核心可能比我想象的小得多

更准确地说——之前我以为 agent 框架的价值在于"功能多寡和编排灵活度",所以刚开始选框架时纠结 LangGraph 的 graph DSL 优雅还是 CrewAI 的多 agent 编排更聪明。但读完 CC 源码后发现:CC 真正的核心循环量级上只有几千行 TypeScript——剩下绝大多数代码是编程领域的工具、UI、session 管理、文件编辑。pi 的 packages/agent 核心也只有约 2K 行(剩下 ~45K 行是上层 coding-agent harness)。真正的"agent 之所以是 agent"的部分小得多,剩下的全是领域 / 形态 / 商业模型的产品职责。

带着这个判断我去做实践——把 3 个完全不同方向的产品(一个对话型、一个图像生成 + 画布、一个视频生产流水线)迁到同一个我自己写的 Python 微内核(Mauri)上。验证目标只有一个:这套"小核心"能不能真的跨方向跑通

结论:。L1-L3 三层 3 个产品 100% 共用同一份内核;L4-L6 三层各自独立。

2. 一个核心判断:agent 能力靠结构约束,不靠模型自觉

我把这件事单独拎出来讲,因为它是后面所有分层判断的底层逻辑。

之前大家治理 agent 的主流思路之一是 提示词治理——在系统提示里写"请不要做 X" / "请优先用工具 Y" / "请保留关键信息再总结"。这条路的天花板是模型自觉——大模型要"读懂"指令、"记住"约束、"主动"执行。任何一项 LLM 走神就破。这就是为什么同一个提示词在 Opus 4.7 跑得很好,换 GLM 就开始胡说、用错工具、丢任务身份——不是 GLM 不行,是这套治理方式天生依赖模型自觉,弱模型撑不住。

CC 教给我的最重要一件事:真正的 agent 治理是结构约束,不是模型自觉。看 CC 几个关键设计就知道:

  • isConcurrencySafe(input)——并发安不安全是运行时层面结构判断,不是让大模型"小心副作用"
  • 子 agent 的工具过滤(filterToolsForAgent + ALL_AGENT_DISALLOWED_TOOLS——CC 启动子 agent 时把不该让子 agent 用的工具从工具表里物理剔除,大模型根本看不到。这是结构约束的标准范例
  • sub_agent_tool 默认防递归——同理,子循环的工具表里物理上没有 sub_agent 这个工具(CC 的 ALL_AGENT_DISALLOWED_TOOLS 含 Agent 工具),不是让大模型"知道不要套娃"
  • 权限 deny 决策(CC 内部权限枚举是 allow/deny/ask)——直接阻断工具调用执行,不是让大模型"被劝说放弃"
  • 保留标记式的上下文压缩——内核物理保留决策轨迹,不是让大模型"自己记住关键决策"
  • 强类型事件协议(Event 强 schema)——非合规的协议格式在解析层就炸,不是让大模型"理解协议"

这 6 件全是结构约束。大模型不需要"知道"、不需要"自觉"——它在结构上就只能做对的事。这跟提示词治理的核心差别在哪?提示词治理依赖模型理解;治理层(harness)治理依赖系统结构。前者能力上限跟着模型走,后者能力上限跟着架构走。

这就是 harness = 治理 的含义。Agent 这个东西的治理层不在提示词,在 harness——也就是包裹大模型的那层结构(内核 + 工具契约 + 模态 + 权限 + 上下文压缩)。我们说"做 agent"做的其实是 harness,不是 LLM。所以治理 agent 这件事更应该从优化架构来思考,不是从优化提示词来思考

理解了这条之后,回过头看 L1-L7 七层模型就很清楚——L1-L3 三层全是结构约束的具体形态:L1 用类型协议锁住模型↔Agent 的事件契约,L2 用一组钩子 + 模态锁执行流,L3 用工具契约 + 并发分类 + 子 agent 锁能力边界。这 3 层加起来就是治理层的硬骨架。L4-L6 三层是产品形态分化,L7 是元层(meta layer)驱动 L1-L6 改动。七层模型本质上是"治理层的解剖图"——把治理 agent 这件事拆成 7 个可独立思考的子问题。

下面按层展开。

3. L1-L7 七层模型 — 治理层的解剖

这套分层不是凭空抽象,是从 3 个实现里反推出来的。每层回答一个具体的"治理层该怎么约束"的问题:

L 层 这一层 harness 约束什么 是否收敛 L1 协议层(Protocol) 大模型 ↔ Agent 之间的事件契约 3 实现结构性一致 L2 内核层(Kernel) Agent 的执行流骨架 3 实现结构性一致 L3 工具层(Tool) Agent 的能力边界 3 实现结构性一致 L4 知识层(Knowledge) Agent 的长程记忆 schema 各领域独立 L5 界面层(Surface) 用户跟 Agent 怎么交互 各形态独立 L6 运营层(Ops) Agent 作为产品/SaaS 的运营 各商业模型独立 L7 评测层(Eval) Agent 怎么演化 元层驱动 L1-L6 改动

L1-L3 三层是 治理层的硬骨架——本质问题只有一组解,结构约束就这一种合理形态。L4-L6 是 治理层的软包装——形态问题每个领域都不同。L7 是元层——它的输出反过来驱动 L1-L6 演化。

4. L1 协议层 — 事件契约的结构约束

L1 定义两件事:

  1. 模型适配器(adapter)契约:每个 provider 实现同一个 adapter.stream(...) 接口,返回一个异步迭代器(async iterator),产出 Event 事件
  2. 事件类型(Event)的判别联合:所有 provider 流出的事件归一到一个带类型标签的统一类型——TextStart / TextDelta / ToolCallStart / ToolCallInputDelta / ToolCallEnd / ToolResult / Thinking / Usage / Stop 等几个变体

L1 之上所有层都消费这个事件流,不直接见 provider 原生 SDK。这就是 L1 的结构约束作用——下游代码不可能依赖 provider 私有字段,因为统一类型根本不允许。

CC、pi、Mauri 都收敛到这套形态(各实现的事件变体名略有出入——比如 pi 把工具结果作为单独 Message、终止用 done/error,没有独立的 ToolResult/Stop 事件;这里讨论的是 L1 的抽象形状,不是变体名一对一映射)。为什么"只有 1 种"? 因为大模型 provider API 本身就 1 种结构——流式产出 token + 工具调用 delta + 用量计量 + 停止原因。实际跨 provider 分两个兼容家族——OpenAI Chat Completions 和 Anthropic Messages API,其它 provider 基本通过这两家的 base_url swap 覆盖;两个家族的抽象骨架是一样的。我在跨 provider 矩阵测试里验证过——任何 adapter 偷工减料矩阵立刻报红。这套协议格式的约束力比想象的强——它把 L1 形状几乎完全确定了,留给设计者的自由度只在"事件变体怎么起名"和"provider 私有扩展往哪个容器塞"。

L1 一个容易踩的反模式

最常见错误是把 provider 私有扩展字段直接塞进 Event 统一类型——比如 Anthropic 的 cache_control / OpenAI 的 function_call 兼容 / Zhipu 的 reasoning_content。Event 一旦变成"所有 provider 字段并集",跨 provider 抽象就废了。

正解是 Event 只保留所有 provider 都有的语义——Mauri 干脆不给 Event 留任何"自由形态容器",私有字段在 adapter 内部直接消化掉不外露(Anthropic 的 cache_control 在 adapters/anthropic.py 内部处理、OpenAI 的 function_call 兼容由 adapter 翻译),Event 联合保持绝对纯净的跨 provider 共同语义。真正需要"自由容器"的是跨层钩子上下文 dict 那类传递,Mauri 这块专门写了一条"载荷契约纪律"——每个触发点(fire-site,往里写值的地方)必须用类型化字典锁住键名契约,避免后续读取点(read-site,从里读值的地方)键名漂移导致静默失效。

关键不变量

  • stream() 返回异步迭代器而不是可等待的协程(这是 pi 早强调的一条;如果写成 async def stream() 返回协程,跨 provider 矩阵立刻抓出来)
  • 至少一个 Usage(用量)事件先于 Stop(停止)事件
  • 停止原因归一到固定的字面量集合

5. L2 内核层 — 执行流骨架的结构约束

L2 是真正的微内核——主循环 + 钩子 + 模态 + 上下文压缩。这层收敛最齐。

5.1 ReAct 模式的异步生成器主循环

骨架就是 ReAct:调大模型 → 解析工具调用 → 调度执行 → 把结果填回历史 → 下一轮。所有 3 个实现都是这个形状。但有一条容易踩——主循环必须是异步生成器,事件即时产出不缓冲

async def loop(...) -> AsyncIterator[Event]: while not should_stop: async for event in adapter.stream(...): yield event # 立刻产出,外层马上看到 if isinstance(event, ToolCallEnd): pending_calls.append(...) for call in pending_calls: result = await dispatch(call) yield ToolResult(...)

反模式:先把流收成一个 list 再批量发送。这就是 agno 默认情况下踩的同一个坑(agno 有 stream=True 开关能逐字流式,只是默认是 buffered)。CC 的 query.ts 是异步生成器,pi 在 L1 协议层的 stream() 也确实返回异步迭代器而非可等待的协程;但 pi 在 L2 主循环这一层走的是回调 emit + EventStream 包装器——runAgentLoop 本身是返回 Promise 的普通 async 函数,不 yield 任何东西,对外的增量流式通过外层 agentLoop() 包装。所以收敛点的精确表述应该是"增量、可中断的类型化事件流",不是"异步生成器这一具体机制"——pi 证明了 TS 里回调 emit + 干净的 EventSink 抽象也能达到同样的语义效果。Mauri 在 Python 里用异步生成器是最自然的写法,一开始写错过(写成先收集再批发),跨 provider 矩阵 + SSE 流畅度调试才改对。

为什么 Mauri 在 Python 里必须用异步生成器?因为 ReAct 主循环内部有多层嵌套(上下文压缩 → on_pre_llm 钩子 → 调用 adapter.stream → on_event 钩子 → 工具调度 → on_post_tool 钩子 → 下一轮),任何一层想"事件即时传出去"如果用回调模式就要把整个调用栈翻成续延传递风格(continuation-passing style)。异步生成器让每一层都可以产出事件,外层自然向上传播——这是 Python 里最自然的写法。pi-mono 走了另一条路:L1 的 stream() 是 async iterator,但 L2 的 runAgentLoop 是普通 async 函数 + 回调发射事件(caller 传入 onEvent 拿流),外层用 EventStream 包一层做迭代——这两种写法都解决同一个嵌套传播问题。

5.2 三注入点钩子架构 — 钩子是结构化的扩展点

CC 在 HOOK_EVENTS 里枚举了二十几个事件名(PreToolUse / PostToolUse / UserPromptSubmit / PreCompact / Stop / SubagentStop / SessionStart / SessionEnd / ...)——但语义维度上仍是 3 阶段 + 生命周期:装配(Assemble)/ 推理(Model)/ 执行(Execute)+ 暂停/停止/压缩前后。pi 在核心层(packages/agent)有 4 个钩子:onPayload / onResponse(请求-响应级)+ beforeToolCall / afterToolCall(工具级,agent-loop.ts 里实际调用);更丰富的压缩 / LLM 前 context 等事件在上层 coding-agent harness。Mauri 的钩子按同样的 3 阶段切:

阶段 时机 Mauri CC pi 装配(Assemble) 造请求时 on_pre_llm UserPromptSubmit onPayload(核心层) 推理(Model) 拿响应时 on_event adapter observer onResponse / stream observer 执行(Execute) 调工具时 on_pre_tool / on_post_tool PreToolUse / PostToolUse beforeToolCall / afterToolCall(核心层)

为什么是 3 阶段而不是 4 个或 12 个?ReAct 主循环只有这三个 "大模型 ↔ 外部世界" 的交界面:装配阶段是给大模型喂、推理阶段是大模型自己说、执行阶段是大模型让外部干活。任何钩子要么落这三段,要么属于生命周期。我尝试过加第 4 阶段("渲染前最后一刻" / "大模型内省" 之类),最后都被证明可以用现有 3 阶段 + on_event 表达。3 阶段不是设计者选的,是 ReAct 主循环的拓扑天生决定的。CC 的 HOOK_EVENTS 数目多是因为细分了生命周期(会话开始/结束、子 agent 停止、通知等),不是发明了新阶段。

更重要:钩子是治理层的扩展接口。产品想加新的结构约束(不是新提示词指令),就挂钩子。比如"agent 应该什么时候停"的判断,不是写进系统提示让大模型自己判断,而是挂 on_pre_compact / on_stop 钩子让内核结构上判断。钩子是结构约束的产品侧延伸。

5.3 ModeProfile 模态 — 模态的结构约束

CC 有 plan 模式(实际权限模式枚举是 default / plan / acceptEdits / bypassPermissions / dontAsk,所以严格说没有"edit 模式",但口语上大家这么对应)。CC 的 plan 模式是系统提示叠加 + 权限层 deny 兜底——messages.ts 里明文写 "Plan mode is active… you MUST NOT make any edits… This supercedes any other instructions",并在权限层把非只读工具全 deny;模型在 plan 模式下仍然看得见所有工具,只是调用被拒。所以诚实说,CC 的 plan 模式部分依赖系统提示(这正是我前面批的"提示词治理"),只是有权限层兜底,没让模型纯靠自觉。

Mauri 把"模态"抽象成 ModeProfile 6 字段name 标识 + 5 行为字段(system_overlay / visible_tool_names / permission_default / compaction_keep_extra / stop_predicate)。其中 visible_tool_names 是 Mauri 自己加的——直接把不在白名单的工具从模型可见的工具表里物理剔除,做到"提示词不需要写,模型看不到就不会调"。这条灵感来自 CC 的子 agent 工具过滤filterToolsForAgent),我把同样的思路从子 agent 扩到了模态。pi 在 harness 层手搭,没有显式 ModeProfile 数据结构。

跨产品负荷实证:当前 4 字段已落地 + 2 字段预留——3 产品里有 2 个(对话 + 视频)真用 ModeProfile,都用到前 4 字段(name + system_overlay + visible_tool_names + permission_default);后 2 字段(compaction_keep_extra / stop_predicate)是内核预留接口(docstring 自标 "ignored today"),等 2+ 产品同语义触发再做。CC 的 plan 模式更接近 2 字段(系统提示叠加 + 默认权限),Mauri 比 CC 多的真正在用的两字段是 visible_tool_names(物理剔除工具)+ permission_default(模态级默认权限)。

5.4 保留标记式上下文压缩 — 身份保持的结构约束

CC 的 microCompact:找到老的工具结果消息,把内容主体用一个占位字符串原地覆盖,保留外壳(消息的位置、ID、关联关系)。压缩不调大模型。注意 microCompact 本身只是覆盖——它不做归档;CC 的 toolResultStorage 是另一个独立机制(按 tool_use_id 把原始结果存到磁盘 / 索引侧),让 UI 或后续步骤可以按 ID 再取。这两件事在 CC 里是分开的两层,不要混成一个"压缩存归档"。Mauri 借鉴的是 microCompact 这条——保留外壳、占位主体、压缩零 LLM 调用——归档不在 mauri 内核做(L4-L5 那是产品职责)。

这条原则不是单点踩坑想清楚的,是 3 个产品各踩了一种独立的压缩反模式后逼出来的:

对话产品(基于 agno):用 agno 自带的 num_history_runs=3/5(只喂最近 N 轮)+ use_session_summary(自动 LLM 摘要老对话)。前者让早期决策物理消失,后者把工具输入输出 / 用户接受 X 拒绝 Y 这些决策细节摘没。朴素压缩反模式——单看都合理,叠起来 agent 在长会话里早就丢了决策骨架。

视觉产品:超 100k tokens 触发次级模型摘要——读着对、文字流畅,但丢了决策轨迹。主模型只看"做了 Y"看不到具体结果,所以重复调工具、重复讨论已经定过的方案。摘要式压缩反模式——重复行为的根因不是模型笨,是摘要把决策证据撕了。

视频产品:机械翻译 CC 的 marker-preserve(替换老 tool result 为 [content cleared — reference by revision_id]),但字面照抄没理解 CC 为什么这么做。结果撞到二次增长 bug:重复压缩会把"已清除标记的消息"再归档,O(N²) 增长且正在悄悄覆盖原始内容。内核层补上保留标记的幂等性才修对。

3 个独立失败模式让我看清——压缩必须保护的不是"信息密度",是 session identity(用户原始意图 + 决策日志 + 工具调用序列 + 当前模态 + 已接受 artifact)。这是 Mauri SessionIdentity + MicroCompactPolicy 的来源。

原则收口:压缩是身份保持,不是摘要。"让模型再读一遍历史重新写"——不管主模型还是次级模型——依赖大模型自觉保留决策轨迹(它不会);保留标记式压缩是内核物理保留决策轨迹(结构上不可能丢)。

6. L3 工具层 — 能力边界的结构约束

两条 CC 独家发明的设计在生态里扩散开了。

6.1 子 agent 作为工具(Sub-agent via Tool)

CC 的 Task(现内部名 Agent)工具——主循环不变,子 agent 是一个普通工具,工具的处理函数内部启动一个子循环,子循环完成后把最终文本回给父循环。Mauri 借鉴了这套设计原则——子 agent 作为工具而非独立编排实体。pi 在 harness 层手搭。

为什么是结构约束:防递归不靠模型自觉。子循环的工具注册表里物理上就没有 sub_agent 这个工具(除非显式 allow_nested=True 打开嵌套)。大模型不需要"知道"不能套娃——它在工具列表里看不到 sub_agent。

LangGraph 走显式图(graph)路线,但我观察到的实际项目,图最终退化成"主 agent + 几个固定子任务"——只是用图 DSL 表达了一次然后退化回来。CC 设计者大概早看穿这点。

6.2 is_concurrency_safe(input) — CC 独家发明的扩散

CC 的 Tool.ts:402 有 isConcurrencySafe(input): boolean——接受输入参数的回调。运行时在同一批工具调用里对"安全=true"的并行调度、对"安全=false"的串行。

# 视觉产品 generate_image 的真实并发谓词(3 case 区分):def _concurrency_safe(input): if input.get("source_image"): return True # 显式 anchor,已 resolved if input.get("__pending_chain"): return False # agent 层 late-inject 前,必须串行 return True # 独立 variant 生成(如 "4 种风格探索"),可并行
@tool(is_concurrency_safe=_concurrency_safe)async def generate_image(input): ...

简化版的 lambda input: input.get("chain_to_previous") is None 我早期写过——结果发现"4 个独立 variant 调用"也被打成串行,触发 agent 层 auto-chain 把第 1 张结果注入到 2/3/4 的 source_image,污染了整个 cohort。这条踩坑直接逼出 3-case 分类。

pi 也有并发分类,但形态不同——pi 的工具有 executionMode?: "sequential" | "parallel" 静态标注(types.ts:330),默认 parallel,整批里只要有一个 sequential 就回退串行(agent-loop.ts 走 executeToolCallsParallel,内部 Promise.all)。所以 pi 独立地也收敛到了"部分并行部分串行"的工具并发分类——这条其实强化了 L3 收敛论点。CC 的真正精进在于:把分类做成 per-call 按输入参数动态判断——同一工具 generate_image 普通 call 安全可并行,带 chain_to_previous 引用前一张图的 call 必须串行;pi 是 per-tool 静态标注,表达不了这个"同工具不同 input 不同安全性"。在编程 agent 场景 per-tool 标注够用;但视觉生成场景(同样是 generate_image,input 决定能否并行)就需要 CC 那种输入感知的动态谓词了。

为什么是结构约束:并发安全不靠模型"小心副作用"——它在调度层结构判断。大模型输出 8 个工具调用,运行时自己分析哪些可并行哪些必须串行。大模型不知道也不需要知道。

6.3 工具修复(Tool repair)— 输入修复的结构约束

CC 的 backfillObservableInput 是单工具内嵌——大模型给的输入缺上下文相关字段,处理函数调用前补上。Mauri 抽象成统一 RepairContext + repair: Callable[[input, ctx], input] 框架。pi 没这层。结构约束:大模型不需要"记住每次都把 size 字段填全"——修复函数会自动补。大模型失忆 OK,结构兜底。

7. L4 / L5 / L6 — 故意不收敛

L1-L3 是治理层硬骨架。L4-L6 这三层每个项目分化。

L4 知识层(内容跟领域走):CC 这块栈最厚——CLAUDE.md + skills + /memory + Auto memory(跨 session 自动写学习笔记)一整套。pi-mono 也做了——AGENTS.md/CLAUDE.md 自动 discovery + 完整 Agent Skills 标准,只是没做 auto-memory。Mauri 完全不做 L4——只暴露 system_overlay 插槽。内容 schema 跟领域走:编程要项目结构 + 编码风格,图像生成要设计系统 + 品牌规范,视频生产要角色设定 + 镜头语言,一套 schema 服务不了这几个方向

L5 界面层(形态跟产品走):CC 跨 CLI(claude 终端)+ VS Code / JetBrains IDE 扩展 + Claude Desktop + Managed Agents 云端 Web 多种形态。pi 有 pi-tui(终端 UI)+ pi-web-ui(web chat 组件)两套。我 3 产品是 React + SSE / React + 画布 + SSE / HTTP 轮询。Mauri 完全不做 L5——内核只发出事件,产品决定怎么渲染。

L6 运营层(商业模型决定):不同 agent 产品的计费机制差异很大——订阅制、按消耗积分扣费、钱包级 ACID(reserve / settle / reverse)、被动 cost tracking、Stripe / 微信支付前置拦截等等,抽象度、并发模型、enforcement 强度都不一样。任何一种硬塞进内核都会让另一些产品变扭——所以 Mauri 内核不绑商业模型,钩子挂事件流让产品自己接。

8. L7 评测层 — 演化引擎

L7 是可观测性 + 评测。它的输出反过来决定 L1-L6 怎么改

CC 内部的 eval 框架未公开。pi 强调的是 伪 provider(faux provider)+ 跨 provider 测试矩阵——这是 pi 最值得借鉴的纪律。

pi 的 packages/ai/test/* 有 20+ 条跨 provider 矩阵测试,强制每个 provider adapter 都过。代表性的 9 条:

  1. tokens.test — 至少一个用量事件出现在停止事件之前
  2. abort.test — 中途取消在 ~1 秒内产出停止事件
  3. empty.test — 空用户消息正常产出停止事件
  4. context-overflow.test — 上下文超长归一到统一类型化错误码
  5. total-tokens.test — token 总量统计在停止事件里能正确闭合
  6. unicode-surrogate.test — 残缺的 Unicode 代理对不会让流崩
  7. tool-call-without-result.test — 工具调用没有结果跟随也能干净结束
  8. image-tool-result.test — 带图片的工具结果能往返
  9. cross-provider-handoff.test — Provider A 起的对话能在 Provider B 续上

这些项就是 L1 不变量的具体化——provider 抽象层的正确性证明。Mauri 借鉴了 pi 这套测试矩阵的设计纪律,做了自己的 11 项核心矩阵(在 pi 9 项基础上加 image_limits / stop_reason_normalization / usage_accumulation 三项 mauri 内核约束)。没这套测试矩阵的 provider 抽象就是裸奔

加上伪 provider + 主循环级别的评测——L7 让 L1-L6 改动可证伪。没 L7 你做架构靠手感,有 L7 你做架构靠数据

9. 3 实现交叉对比

L 层 Claude Code pi-mono Mauri L1 协议 Anthropic Messages API 形状的 Event;多 provider 通过 Bedrock / Vertex AI SDK 等独立适配层 类型化判别联合 + 泛型 StreamFunction<TApi, TOptions> 判别联合(dataclass + Literal) L2 主循环 query.ts 异步生成器 runAgentLoop(async 函数 + 回调 + EventStream 包装) mauri.kernel.loop 异步生成器 L2 钩子 HOOK_EVENTS ~20+(语义上 3 阶段 + 生命周期) 核心层 4 个:onPayload/onResponse + beforeToolCall/afterToolCall;压缩/上下文事件在 harness 层 9 个(3 阶段 + 生命周期) L2 模态 plan 模式 = 系统提示叠加 + 权限 deny 无显式模态(harness 手搭) ModeProfile 6 字段name + 5 行为字段(system_overlay / visible_tool_names / permission_default / compaction_keep_extra / stop_predicateL2 压缩 microCompact(占位字符串覆盖工具结果)+ 独立的归档存储机制 harness 层 MicroCompactPolicy + 标签跳过 L3 工具契约 Tool 接口 + isConcurrencySafe(input) per-call 独家 Tool 类型 + executionMode: "sequential" \| "parallel" 静态注解(默认 parallel) @tool 装饰器 + RepairContext + 并发分类 L3 子 agent Task(现内部名 Agent)工具 + filterToolsForAgent 工具过滤 harness 手搭 sub_agent_tool 工厂方法 L4 知识 CLAUDE.md + skills + /memory + Auto memory + Managed Agents memory + Dreaming AGENTS.md/CLAUDE.md discovery + Agent Skills 标准全实现(无 auto-memory) 完全不做system_overlay 插槽) L5 界面 CLI + IDE 扩展(VS Code / JetBrains) + Desktop + 云端 Web pi-tui + pi-web-ui 完全不做(只发出事件) L6 运营 无计费层 无 完全不做(钩子挂事件流) L7 评测 内部 eval 未公开 伪 provider + 20+ 项跨 provider 矩阵(pi 最强项) 11 项核心矩阵 + FauxAdapter

3 实现 L1-L3 结构性一致,命名差异但骨架同。L4-L6 三层 Mauri 完全选不做——微内核原则。L7 Mauri 学 pi

几个观察:

  1. Mauri 的 ModeProfile 5 行为字段 vs CC plan 模式 2 字段——不是过度抽象,是跨方向测试逼出来的。跨 3 产品负荷下 5 字段砍掉任一个都会让某产品破。CC 是单一编程领域所以 2 字段够。
  2. pi 在 L2 核心层暴露 4 个钩子(onPayload/onResponse + beforeToolCall/afterToolCall) vs Mauri 9 个——pi 把压缩前后、on_event 逐字推理这些钩子留给 harness 层自挂,没在核心层硬编码。Mauri 在内核里固化了 on_event(推理阶段)和 on_pre_compact——前者是因为产品要做逐字流式 UI 渲染,后者是因为长会话压缩需要钩子进入压缩前后状态。这是一个核心层 vs harness 层的取舍,不是 pi "少做了"。
  3. L4-L6 全是 Mauri "完全不做"——这是最关键的纪律。任何一项放进内核都会绑死一种产品形态/商业模型/方向,跨 3 产品就跑不通。

10. 3 产品迁移的实测数据

3 个产品起步时方向几乎不重叠:

产品 方向 与 Mauri 内核的集成形态 对话型 agent 通用对话 + 富媒体输出 agno 壳 + Mauri 内核接入(项目元数据仍是 agno-demo-app,runner 层调 mauri.kernel.loop) 视觉型 agent 图像生成 + 前端画布编辑 + 钱包级 ACID 计费 自建 core/ + Mauri 内核渐进合并(compaction_bridge 桥接 + use_mauri opt-out 留逃生舱) 视频型 agent 视频生产流水线 + 被动 cost tracking Mauri 内核为主(已 pin mauri>=0.3.1,接入最深)

极少工具重叠。但迁完之后:

  • L1 协议层:3 产品都接入了同一份 Event 联合类型
  • L2 内核层:9 钩子 + 停止判定共用同一份;ModeProfile 模态栈在 3 产品里 2 个用(对话 + 视频,视觉产品没用到)
  • L3 工具层:Mauri 的 RepairContext 共用;并发分类调度,视频产品直接用 @tool(is_concurrency_safe=...),视觉产品在自建 core/tool.py 里实现了形态等价的 is_concurrency_safe(input) callable
  • L4 / L5 / L6 各自独立

3 个方向独立但内核骨架共用——L1-L3 都在用同一份 Mauri 内核(哪怕接入深浅不一)。这件事本身就是 L1-L3 收敛架构正确性的实证。

举一个具体故事——长会话压缩归档的二次增长 bug

对话产品先在 50 轮长会话遥测里捞到这条:观察到 579 store_call_count / 480 archive entries,而朴素预期是 44(11 次触发 × 每次 4 条归档),实际~10× 预期。同步项目里推演了下放大曲线:200 轮约 64× overhead、1000 轮约 227× overhead。后来视频产品在干净版本的 wrapper 上重跑同一类压缩,触发出 1075 store_calls——比对话产品的 579 还放大了 2.5×。两边都把"50 轮稳定上界"压破,逼出内核中期补丁。调试进去发现根因——压缩策略每次压缩会把"已经是清除标记的消息"再归档一次,写到同一个修订号键把原内容覆盖掉,再发新标记。这是 O(N²) 二次增长,而且正在悄悄覆盖原始内容

修复是 1 行幂等检查:if m.content.startswith(_CLEARED_MARKER_PREFIX): continue。修完后实测落到 46 store_calls = 理论下界(50 轮 × 每轮 1 工具 − 4 keep_recent = 46 条要归档的 ToolResult,每条精确归档一次)。但这个 bug 在 50 轮以下完全不可见——3 个产品早期开发都跑不到 50 轮,直到对话产品第一个跑长会话遥测才暴露。短会话设计正确未必意味着长会话健壮

再举一个——钩子载荷键名漂移导致静默失效:对话产品有一组内置钩子读 PRE_STOP 触发的载荷。某次 PR 引入新的 PRE_STOP 触发点,开发者写载荷用了 stop_reason / request_user_message / tool_calls_completed_total 这种键名。但内置钩子读的是 last_stop_reason / user_message / tool_calls_completed——两边键名不一致,钩子触发但内置读不到任何值,完全静默失效。类型检查抓不出来(字典接受任何键),流式协议层抓不出来(钩子本身有触发),只在用户报"任务永远不结束"才发现。修完之后专门写了一条载荷契约纪律——任何钩子触发点必须用类型化字典锁住键名契约。这条经验直接喂回了 L2 内核设计

类似 surface 还有十几个。每个都是一次"这个边界该在内核还是产品"的微决策。收敛架构的可信度不是从 3 个项目的相似性来,是从十几个 surface 探测出来的边界稳定性来

11. 5 件收敛不到的反面证据

诚实点的话,L1-L3 收敛不是万能。我在视觉产品迁移调研里发现 5 项 wrapper 职责跟 9 钩子词汇结构性不兼容:

  1. 同批次内同伴互改——同一批工具调用里 sibling 互读结果。on_pre_tool 和 on_post_tool 是单调用钩子,没有同批次视图。
  2. 钩子产出额外事件——钩子想往内核事件流里产出额外事件。但 on_post_tool 只返回单个工具结果。
  3. 语义优先级排序——同一批次内"前置说明工具"必须比"主生成工具"先跑。并行/串行二分不表达"串行集内优先级"。
  4. 结果时刻的多源解析——某个 ID 字段的三层解析必须在状态更新后、领域块发射前的时机。on_post_tool 是单调用视图。
  5. 传输错误重试-继续——临时错误重试需要钩子能拦截 adapter 异常。on_event 是观察者,没拦截语义。

这 5 项都没收敛到通用微内核。触发条件是 2+ 产品同语义,目前都是单产品观察,未达门槛。

为什么本质不可统一进 9 个钩子?共同特征:钩子想做产出、想做同伴协调、想改控制流、想拦截异常。而 9 钩子词汇是按观察者 + 转换器设计的。两边能力维度差一档

要满足这 5 件需要让钩子升级成"内嵌在调度循环里的协程"——但这会让内核 API 复杂度从 ~50 行变成 ~300 行,是框架化滑坡的起点。所以我现在的选择是承认这 20% 在产品侧。CC 的 HOOK_EVENTS 本质也是"观察者 + 单调用转换器"形态——它能拦截/修改单个工具调用,但不暴露这 4 种能力的产出/同伴协调/控制流/异常拦截语义。收敛的边界本身就划在 80/20

12. 我的结论 — 7 层各做 / 不做 / 等触发

L 层 做 不做 L1 判别联合事件 + adapter 契约 + 跨 provider 矩阵 provider 私有字段进统一类型 L2 ReAct 异步生成器主循环 + 9 钩子在 3 阶段 + ModeProfile 5 行为字段 + 保留标记式压缩 让大模型来总结 / 多 agent 图 DSL L3 @tool 装饰器 + sub_agent_tool + is_concurrency_safe(input) per-call + 工具修复 领域特定工具进内核 L4 只暴露 system_overlay 插槽 skills 框架 / CLAUDE.md 式统一格式 / 长期记忆 schema L5 只发出事件;产品自己渲染 CLI / TUI / Web 统一抽象 L6 通过钩子暴露事件流 计费 / 配额 / 审计进内核 L7 伪 provider + 跨 provider 矩阵 + Checker 扩展性 单一打分体系

等 2+ 产品同语义触发再做:§11 那 5 项 wrapper 职责(同伴互改 / 钩子产出额外事件 / 语义排序 / 多源解析 / 重试-继续),加上更早记录的待触发项(会话身份升一级概念 / 懒注册机制)。

"不做"和"等触发"清单比"做"清单更重要——它防住了框架膨胀这条所有 agent 框架都会陷进去的失败模式。LangGraph / CrewAI / AutoGen 早期都是好框架,问题出在复杂度不断膨胀——新概念、新 DSL、新抽象一层叠一层,最后用户认知负担超过了收益。

13. 写在最后

跑完这一轮——从 agno 流式痛点到 openclaw 启发到读 pi 源码到看 CC 泄露源码到自己做 Mauri 跑 3 产品迁移——我们验证了一个判断:

Mauri 不是"我做了一个属于自己的框架",而是"我独立验证了同一收敛点的第 3 个实例"

前者是个人作品 → "看,我也做了一个"
后者是数据点 → "Python + 跨方向也能落到同一 L1-L7 结构 = 架构本身是结构性的,不是 TypeScript 特定 / 编程 agent 特定"

第 3 个独立数据点比第 1 个个人作品有信息量得多

更重要的是——这一轮把我对 "怎么治理 agent" 的判断彻底定义。治理 agent 不是优化提示词,是优化 harness。harness 就是 L1-L7 七层的硬骨架。L1-L3 结构约束做对了,弱模型也能跑稳;L1-L3 没做对,强模型也会幻觉、用错工具、丢任务身份。

这是为什么 CC 源码被社区反编译这件事能让全球 agent 能力进步 20%——大家学到的不是"CC 怎么写提示词",是 "agent 这个东西怎么用结构约束去治理"。pi-mono 早就在做同一件事(packages/ai 的跨 provider 矩阵就是结构约束的极致表达),只是它的知名度可能不如 CC。

Mauri 现在不开源——但这篇文章里的 L1-L7 模型 + 3 个实现交叉对比 + 我 3 产品迁移得到的实测数据 + 5 件反面证据,是我这段时间的全部 takeaway。如果你正在做 agent 系统,停下来想清楚 harness 怎么设计——比任何"框架选型"建议都有用得多。

如果你跑完类似实践得到不同的收敛结论——某层在你方向不成立、或者发现该收敛的第 8 层——我想看你的数据。收敛假设的价值不在于它"对",而在于它可证伪

最后说一句:这套思考是我在做密集开发的间隙慢慢沉淀的,谈不上完整,也谈不上完全正确。但写出来比闷头继续做更有意义——如果有几十个团队各自把自己的实践数据 + 反面证据公开,整个生态可能再进步一轮。

附录:项目链接

  • CC代码就不附链接了,应该很容易可以找到。
  • github.com/earendil-works/pi — pi(作者 Mario Zechner / badlogic,libGDX 创建者)。原仓库地址是 badlogic/pi-mono,npm scope @mariozechner/*;后来项目迁到 earendil-works 组织。

—— Mauri(我自己写的 Python 微内核)暂不开源,但你完全可以根据以上写一个属于你自己的微内核,如果未来开源会更新这条注脚。

读到这里如果你觉得这套 L1-L7 模型对你有帮助、或者想分享你自己的反面证据,欢迎交流——我一直在持续探索这件事,希望多一些同行视角。

注:全篇文章由我和CC共同完成,我起草具体内容以及大纲结构,CC根据我的开发记录来填充细节,最后由我来审核修改。