这不是"调一次 API 就完了"的简单对话——这是一个真正的 Agent Loop。

一、Agent Loop 全景:从消息到回复的完整链路
当你在 WhatsApp 里发了一条"帮我看看 openclaw 项目里有多少个 TypeScript 文件",整个链路是:
WhatsApp 消息到达 Gateway→ Routing Engine(验证 allowFrom)→ Session Manager(查找/创建发送者的 Session)→ Agent Loop 启动:① 构造 System Prompt ← AGENTS.md + CLAUDE.md + .cursorrules② 注入 Skills ← workspace skills + shared skills + plugin skills③ 绑定 Tools ← 内置工具 + plugin 注册的工具 → TypeBox Schema → JSON Schema④ 调用 LLM API ← 主 provider → 备1 → 备2(failover chain)⑤ 模型返回 tool_call:{name: ”search_files”, args: {pattern: ”*.ts”}}⑥ 执行工具 → 拿到结果 → 回传模型⑦ 模型再推理 → tool_call:{name: ”terminal”, args: {command: ”find ... | wc -l”}}⑧ 执行工具 → 拿到精确数字 → 回传模型⑨ 模型生成最终回复:”这个项目有 1,247 个 .ts 文件”← 流式 event:agent 推送(每个 step 都可见)← Channel Adapter 反向适配 → WhatsApp 发送
整个循环的核心是 ReAct 模式:Reasoning(推理)→ Acting(调用工具)→ Observing(看结果)→ 再 Reasoning,直到模型认为任务完成(输出 end_turn)。
二、System Prompt 构造:上下文注入的优先级
Agent 的 System Prompt 不是一段写死的文本——它是多层注入拼接出来的:
项目上下文(最高优先级): AGENTS.md/CLAUDE.md/.cursorrules——告诉 Agent"你是谁、什么项目、什么规则"Skills 注入:所有匹配的 Skills 的 SKILL.md内容——告诉 Agent"你有哪些能力、在什么场景下怎么做"Tools 定义:所有可用工具的 JSON Schema——告诉 Agent"你能调什么函数" Session 上下文:当前会话的历史消息、用户偏好、记忆
注入顺序很重要:Skills 必须在 Tools 之前。如果顺序反了,模型会先看到 50 个工具然后才看到 Skills 说"在这个场景下用这个工具"——模型可能已经基于"第一印象"在心里选错工具了。
三、Tool Binding:TypeBox Schema 的类型安全屏障
Agent 能调什么工具,完全取决于 System Prompt 里注入了什么 JSON Schema。这些 Schema 不是手写的——手写 JSON Schema 极易出错(你写了 type: "string" 但代码里处理 number,编译期完全不报错)。
OpenClaw 的解决方案:每个工具用 TypeBox 定义入参 Schema:
const SearchFilesSchema = Type.Object({pattern: Type.String(),path: Type.Optional(Type.String()),target: Type.Union([Type.Literal(”content”), Type.Literal(”files”)])});
TypeScript 自动推导出类型 {pattern: string; path?: string; target: "content" | "files"}——而实际实现的函数签名也用这个类型。如果 Schema 定义和实现代码不一致,TS 编译直接报错,根本不需要运行时才发现。
四、Session 隔离:per-sender JSONL 存储
每个发送者(WhatsApp 号码、Telegram 用户 ID)都有自己独立的 Session。存储方式:
物理存储: ~/.openclaw/agents//sessions/.jsonl格式:每行一条 JSON,追加写入 隔离级别:不同发送者的文件物理隔离——不是靠 SQL WHERE 子句保证
为什么用 JSONL 而不是 SQLite?
tail -f | ||
对话记录的查询模式是"读某个 Session 的全部历史"——不需要 JOIN, 不需要 WHERE 多条件过滤。JSONL 的简单性在这里是优势而非限制。
同一个人的 WhatsApp 和 Telegram 消息可以路由到同一个 Session——通过 session-key 的跨通道绑定配置。这意味着你在 WhatsApp 上聊了三轮,切到 Telegram 上 Agent 还记得上下文。
五、Provider Failover:为什么需要自动切换模型?
OpenClaw 支持 30+ 个模型供应商。但真正的价值不是你"可以选 30 个",而是你"可以配一条 fallback 链":
主 provider(Anthropic Claude)→ 超时/限流/故障 → 备1(OpenAI GPT)→ 也挂了 → 备2(Google Gemini)→ 还挂 → 备3(DeepSeek)
配置方式:
{”models”: {”mode”: ”merge”,”providers”: {”anthropic”: { ”apiKey”: ”sk-...”, ”models”: [”claude-sonnet-4-20250514”] },”openai”: { ”apiKey”: ”sk-...”, ”models”: [”gpt-4o”] }}}}
每个 Agent run 开始时,Gateway 按顺序尝试 provider,第一个成功响应的就用。这解决了自托管 AI 助手最大的痛点——依赖单一 API 服务,一挂全挂。
六、流式事件推送:用户看到的"正在思考..."
Agent 的推理过程通过 WebSocket 流式推送给所有订阅了 agent 事件的客户端:
status: "thinking"→ 模型正在推理(用户看到"..."或 spinner) tool_call→ 模型决定调用工具(用户看到"正在搜索...") tool_result→ 工具执行完成(用户看到搜索结果预览) text_delta→ 模型逐字生成回复(流式输出,低延迟感知) status: "completed"→ 最终结果
这套事件模型让 Agent 的行为完全透明——你不是在等一个黑盒输出,而是能看到"它先搜了什么、然后读了什么文件、最后怎么得出这个结论"的全过程。
七、Sub-Agent 委派:让 Agent 并行干活
delegate_task 工具让 Agent 能派生子 Agent 并行处理任务。比如:
"帮我同时查一下 DPDK、SPDK、OVS 的最新 release notes"
Agent 可以:
派生子 Agent A → 查 DPDK release notes 派生子 Agent B → 查 SPDK release notes 派生子 Agent C → 查 OVS release notes 三个并行跑,结果汇总后生成统一回复
Sub-Agent 有独立的 terminal session、独立的上下文窗口、独立的工具权限。它们不能调用 clarify(不能反问用户)、不能调用 delegate_task(默认 max_spawn_depth=1 防止无限递归)。
结果通过 Gateway 的事件总线回传给父 Agent——父 Agent 在等待期间可以继续处理其他消息。
总结
Pi Agent 引擎的核心设计原则:
Agent Loop 不是一次调用:ReAct 模式让 Agent 能"思考→行动→观察→再思考" Skills 必须在 Tools 之前注入:模型先读"说明书"再看"工具箱",选错工具的概率大幅降低 TypeBox 保证工具 Schema 的类型安全:编译期报错 > 运行时报错 JSONL Session 隔离:简单、可调试、天然安全隔离 Provider Failover:不依赖单一 API 服务,自托管 AI 的生命线
下一篇拆消息路由——看看 allowFrom、mentionRules、groupPolicy 三层过滤是怎么让"任何人都能给你的 Agent 发消息"变成"只有你允许的人才能触发推理"的。
夜雨聆风