Day 06 / 共 20 天 · 第 2 周 Agent 大脑

LLM 层与 Pi 内核

第 1 周你看清了消息怎么流转、配置怎么塑造助理。第 2 周进入 Agent 大脑——就是消息旅程第④站里那个"想+做"。第一课要打破一个错觉:OpenClaw 自己并不写 LLM 调用循环——它内嵌了第三方 Agent 框架 Pi,自己做"编排外壳"。理解这层关系,本周后面(故障转移、执行循环、提示词、记忆)全通。

📍 你在整门课的位置(第 2 周 · Agent 大脑)
W1 全景· Day6 Pi 内核 Day7 故障转移 Day8 执行循环 Day9 提示词 Day10 记忆· W3 技能/渠道
L01

一个反直觉的事实

🤔 痛点:从零写"会调工具的 AI"有多难? 你要自己实现:拼消息格式、发 HTTP 给 OpenAI、解析它吐的工具调用、执行工具、把结果塞回去、再发一轮……还要兼容 Anthropic/Google 各家不同的接口。光这套"对话循环"就是几千行、坑无数。每个想做助理的团队都重造一遍轮子。
💡 本质:买发动机造车,别自己造发动机 OpenClaw 直接内嵌成熟的 Pi 框架当"发动机"(负责和大模型对话、循环调工具),自己专注造"整车"——多渠道、故障转移、技能、记忆、安全。这就像手机厂商不自己造芯片(买高通),把精力放在做好整机体验上。本周 90% 学的是这层"整车工程",不是发动机内部。

你可能以为 OpenClaw 里有一大段"调 OpenAI/Anthropic API + 解析工具调用 + 循环"的代码。其实没有。真正的 LLM 调用和 ReAct 循环在第三方库 Pi@mariozechner/pi-agent-core / pi-ai / pi-coding-agent,见 package.json:352-355)里。

好比"买发动机造车" 造车厂不一定自己造发动机——可以买一台成熟发动机(Pi),然后专注做底盘、变速箱、内饰、安全系统(OpenClaw 的编排外壳)。OpenClaw 就是这样:Pi 提供"和大模型对话、循环调工具"的发动机;OpenClaw 提供把它变成一个可靠个人助理所需的一切——多渠道、模型故障转移、鉴权轮换、技能、记忆、安全策略。本周你学的 90% 都是这层"外壳"的精妙工程,而不是发动机内部。
⚠️ 常见误解:以为"内嵌第三方框架 = OpenClaw 没技术含量"。恰恰相反——把一个通用引擎驯服成 7×24 不掉线、能接 20+ 平台、还安全可控的产品,难度不比造引擎低。发动机好买,能造出一辆可靠的车才见功力。
L02

Pi 是什么

Pi 是一个 TypeScript 的编码 Agent 框架,分三个包:

  • pi-ai:底层 LLM 传输(streamSimple 真正发请求)。
  • pi-agent-core:Agent 会话、消息类型、StreamFn 接口。
  • pi-coding-agent:编码工具(read/write/edit/grep…)+ ReAct 循环 session.prompt()

OpenClaw 通过 src/agents/pi-embedded-runner/("内嵌 Pi 运行器")来驱动它。入口 createAgentSessionattempt.ts:1824):

import { streamSimple } from "@mariozechner/pi-ai";
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";

({ session } = await createAgentSession({
  cwd: resolvedWorkspace, agentDir,
  authStorage, modelRegistry, model: params.model,
  tools: builtInTools, customTools: allCustomTools,   // ← OpenClaw 注入的工具
  sessionManager, settingsManager, resourceLoader,
}));
读法:OpenClaw 把"模型、工具、系统提示、会话管理器"交给 Pi 的 createAgentSession,Pi 负责跑循环。OpenClaw 的活儿是"准备好这些输入 + 观察输出"。
L03

streamFn 抽象

Pi 用一个 StreamFn 函数接口抽象"怎么和模型对话"。OpenClaw 把不同的传输实现挂到 session.agent.streamFn 上(attempt.ts:1879-1905)。

streamFn = "对话的插座" Pi 循环需要"给它文字、它流式返回模型输出"这么个能力,但不关心背后是 OpenAI 还是别的。这个能力被抽象成一个函数 streamFn——像一个标准插座。OpenClaw 按当前用的是哪个 provider,往这个插座里插不同的"插头"(OpenAI WebSocket 插头、Ollama 插头、默认插头)。这就是我们反复见到的接口/实现分离:Pi 定义接口,OpenClaw 提供实现。
Pi 循环需要一个 streamFn("对话插座"),OpenClaw 层层套壳后插进去 Pi 循环session.prompt()🔌 插座 选插头(L4) ollamaStreamFn OpenAI WS 插头 streamSimple(默认) 装饰器套壳(L5) 缓存跟踪 修剪工具名 修复畸形参数 payload 日志 大模型Anthropic/OpenAI/本地…
streamFn = 标准插座:先按 provider 选插头,再层层套装饰器加能力,最后插进 Pi 循环——Pi 全程无感。
💡 如果让你自己实现(简化版 → 真实版) 你可能会写死:function talk(msg){ return callOpenAI(msg) }——只能用 OpenAI,加个日志/修个参数就得改这个函数。真实版把它拆成"选插头 + 套装饰器":换 provider 只改选插头那步,加能力只加一层装饰器壳,核心 talk 永不动。多出来的每一层,都是为了"不改核心也能扩展"。
L04

Provider 分流

if (params.model.api === "ollama") {
  activeSession.agent.streamFn = ollamaStreamFn;                    // 本地 Ollama 原生 API
} else if (params.model.api === "openai-responses" && params.provider === "openai") {
  activeSession.agent.streamFn = createOpenAIWebSocketStreamFn(wsApiKey, ...);  // WebSocket
} else {
  activeSession.agent.streamFn = streamSimple;                     // Pi 默认(覆盖大多数 provider)
}
读法:三条分支决定用哪个"插头":Ollama 走本地原生 API、OpenAI responses 走 WebSocket、其余全走 Pi 自带的 streamSimple大多数 provider(Anthropic、Google…)都由 streamSimple 统一处理,只有需要特殊传输的才单独分支。
📝 举个例子:三种配置 → 插哪个插头 • 配 model.api = "ollama"(本地跑模型)→ 插 ollamaStreamFn(走本机原生 API)。
• 配 OpenAI 的 openai-responses → 插 createOpenAIWebSocketStreamFn(WebSocket 传输)。
• 配 Anthropic Claude / Google Gemini → 都落到 else 分支,插 Pi 自带的 streamSimple(一个插头覆盖大多数家)。
L05

装饰器中间件

选好 streamFn 后,OpenClaw 用一串"包裹函数"层层增强它(attempt.ts:1925-2059):

// 每一层都接收上一层的 streamFn、返回一个增强版 streamFn
streamFn = cacheTrace.wrapStreamFn(streamFn);                       // 缓存命中跟踪
streamFn = wrapStreamFnTrimToolCallNames(streamFn);                // 修剪工具名
streamFn = wrapStreamFnRepairMalformedToolCallArguments(streamFn); // 修复畸形工具参数
// …… Ollama num_ctx 修正、xAI 参数解码、Anthropic payload 日志
装饰器模式 = "套娃式增强" 每一层都是"拿到一个函数、返回一个功能更强的同类函数"。就像给手机套一层又一层保护壳:防摔壳、防水壳、支架壳——每层加一个能力,但对外还是"一个手机"。这里每层给 streamFn 加一个能力(修工具参数、记日志、跟踪缓存),最外层还是一个普通 streamFn,Pi 循环照常调用、浑然不觉。这是函数式编程里极常见的"中间件/装饰器"手法——把横切关注点(日志、修复、缓存)和核心逻辑解耦。
L06

模型选择

用哪个模型?入口 runEmbeddedPiAgentrun.ts:256)在 run.ts:299-300 定 provider/model(默认值 src/agents/defaults.ts)。插件钩子可在解析前改写:

// before_model_resolve 钩子可返回 { providerOverride, modelOverride }
// resolveModel(provider, modelId, agentDir, config) → { model, authStorage, modelRegistry }
读法:resolveModelmodel.ts)把"provider 名 + 模型 id"解析成真正的模型对象 + 鉴权存储 + 模型注册表。插件可以通过 before_model_resolve 钩子拦截并换模型(Day 12 讲扩展)。模型目录在 models-config.*.ts / model-catalog.ts
L07

插件式 Provider

Provider 既有内建(anthropic/openai/google…),也能由扩展动态注入。比如 extensions/ollama/index.tsregister 只做一件事:

register(api) {
  api.registerProvider({ id, label, auth, discovery, wizard, onModelSelected });
}
读法:想加一个新模型来源(vllm/sglang/ollama),不用改核心代码——写个扩展调 api.registerProvider 即可。src/plugins/providers.ts + provider-discovery.ts 汇入模型配置。这是"开闭原则":对扩展开放、对修改关闭。Day 12 详讲扩展系统。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • OpenClaw 自己写 LLM 循环吗?真正的循环在哪?
  • Pi 的三个包各管什么?
  • streamFn 是什么抽象?为什么要分 provider 挂不同实现?
  • 装饰器中间件怎么增强 streamFn?举两个例子。
  • 怎么加一个新的模型 provider(不改核心代码)?
🎵 记忆口诀(Pi 层一句话) "Pi 造发动机,OpenClaw 装整车;插座选插头,装饰器套壳"——引擎外包给 Pi,OpenClaw 用 streamFn 插座接不同 provider,再层层装饰器加能力。核心永不动,全靠"选 + 套"扩展。

✋ 动手

cd /Users/bitmart/work/codes/github/openclaw
grep -n 'pi-agent-core\|pi-ai\|pi-coding-agent' package.json
sed -n '1870,1910p' src/agents/pi-embedded-runner/run/attempt.ts   # streamFn 分流
grep -n 'wrapStreamFn' src/agents/pi-embedded-runner/run/attempt.ts | head
明天预告 · Day 07模型故障转移——README 说的"OAuth vs API key 轮换 + fallback"是两层嵌套循环:外层换模型、内层换鉴权 profile。这是 OpenClaw"永不掉线"的关键工程。
← 总目录 Day 07 · 模型故障转移 →