
很多人第一次接触 OpenClaw Agents 的时候注意力都放在模型对话、Skill 调用这些看得见的功能上很少有人去关注执行引擎。但我把src/agents/pi-embedded-runner/这个目录从头到尾过了一遍之后我的结论很直接整个 Agent 能不能稳定干活靠的全是这一层。模型负责想Runner 负责转工具负责做而 pi-embedded-runner 就是中间那个承上启下的传动轴。如果你正在尝试部署 OpenClaw、自己写 Skill或者被各种AI 聊天正常但一执行任务就翻车的问题困扰这篇文章就是你排查问题的路线图。我会从设计逻辑拆到实际部署把我自己跑通和踩坑的过程一起放进来。1. 为什么说执行引擎是 Agent 的大脑中枢1.1 从能聊天到能干活的关键一跃你可以把纯粹的 LLM 聊天想象成一个特别能说的朋友——你问他任何问题他都能给你一个语法正确、逻辑自洽的回答但他不会帮你关灯、不会帮你查数据库、不会帮你把文件从 A 目录挪到 B 目录。Agent 要做的恰恰是这最后一步让 AI 的想法变成真实的动作。在这个转变里执行引擎Runner承担的角色非常像操作系统里的进程调度器。聊天场景下模型输出一段文本就结束了但 Agent 场景下模型的输出会被解析成调用哪个工具、传什么参数、期望什么返回的结构化指令。Runner 拿到这个指令之后要去查工具注册表、做参数校验、执行真实调用、把结果重新塞回上下文然后再交给模型进行下一轮判断。我见过不少刚上手的朋友有个误区以为只要把模型 API Key 配好Agent 就能自动聪明起来。实际完全不是这样。你给模型再强如果执行引擎不会解析工具调用、不会管理多轮上下文、不会处理工具异常那 Agent 就只是一个看起来很忙的聊天机器人任务稍微复杂一点就开始空转。1.2 一个典型的思考-行动循环长什么样为了把执行引擎的作用说清楚我先抛出一个最小闭环。一个普通的多步任务在 Agent 内部通常是这样流转的接收用户目标比如帮我查一下当前目录里所有文档中提到了哪些 API 配置项整理成表格。模型规划LLM 把这个大目标拆成子步骤比如第一步列出当前目录文件第二步逐个读取文档内容第三步筛选 API 配置项第四步生成表格。生成工具调用指令模型不直接执行操作而是输出一段结构化的调用意图例如list_files(directory.)。执行引擎调度Runner 解析这个意图找到注册表中对应的工具函数做参数校验然后真正调用。结果回填工具返回的文件列表被写入上下文模型看到真实结果后决定下一步动作。循环直到目标完成或达到上限整个过程可能经历很多轮每轮都是模型输出调用意图 → Runner 执行 → 结果回填 → 模型再输出。你有没有发现这套流程里最容易出问题的环节恰恰不是模型而是第 4 步和第 5 步。工具调用意图解析错了、参数类型不匹配、返回值太长把上下文撑爆、工具抛异常没有捕获任何一个环节出问题整个任务链条就断了。pi-embedded-runner 存在的意义就是把这套流程做成一个稳定、可控、可观测的基础设施。1.3 pi-embedded-runner 在 OpenClaw 里的定位从名字可以拆出三层信息。pi在 OpenClaw 的语境里通常指代 Personal Intelligence也就是面向个人助理场景的智能核心embedded表示它是进程内嵌入运行的不是独立部署的微服务runner则是执行调度器的意思。合起来它的职责很清楚作为嵌入式执行引擎直接跑在你的应用进程里负责把模型输出翻译成真实动作。这个定位带来两个非常实际的好处。第一是低延迟工具调用不需要跨网络请求Runner 和工具函数在同一个进程内省掉了大量序列化和网络开销。第二是便于本地化控制Skill 的执行权限、文件访问范围、网络请求白名单都可以直接在 Runner 层做约束不用依赖外部网关。对个人 Agent 这种既要灵活又要可控的场景嵌入式设计其实比微服务架构更合适。2. 拆解 pi-embedded-runner嵌入式 Runner 的设计逻辑2.1 embedded不是一句口号而是架构选择很多 Agent 框架会把规划器和执行器拆成两个独立服务中间用消息队列通信。OpenClaw 的 pi-embedded-runner 偏偏反着来它把自己做成一个库直接嵌入到宿主进程里。我第一次看到这个设计时也有些疑惑但真正跑起来才明白它的用意。嵌入式架构的核心优势在于工具调用变成了纯函数调用不再需要为每个工具定义一套网络协议。以我接入的一个自定义 Skill 为例函数内部需要读取本地 Sqlite 数据库、调用一个内部 HTTP 接口、再写日志文件。如果是微服务架构我得为这个 Skill 单独封装一个服务接口然后处理认证、超时、序列化但在 embedded runner 的设计下它就是一个普通的异步函数Runner 直接 await 它就可以了。代价也很明显——宿主进程的内存和事件循环会决定 Agent 的性能上限。如果你在同一进程里又跑模型推理又跑大量文件 IO就可能出现事件循环阻塞。所以 OpenClaw 的 Runner 在设计上把模型交互和工具执行基本都做成了异步流程尽量不在关键路径上放同步阻塞操作。你在实践里如果发现 Agent 卡顿可以先检查自己写的 Skill 里有没有同步的文件读操作把它改成异步往往立竿见影。2.2 Runner 如何对接模型层、工具层和记忆层执行引擎不可能孤立工作它必须同时面对三个方向向上对接模型、向下对接工具、侧向对接记忆。pi-embedded-runner 的设计里这三条通道是明确分离的。模型层接口Runner 不关心你底层用的是 OpenAI 兼容接口、Ollama 本地模型还是 Anthropic 的 API它只面向统一的 Completion 接口。这里面有一个很重要的抽象模型输出既可以是纯文本也可以是结构化的工具调用意图。Runner 要做的是把这两种输出都统一成内部消息格式再送给下一步处理。我实际接 Ollama 上的 qwen2.5-3b 时发现小模型对工具调用这种格式的支持不太稳定经常输出一段 Markdown 而不是严格的 JSON优化办法是在系统提示词里明确给例子而不是只描述规则。工具层接口工具在 OpenClaw 里通常以 Skill 的形式存在每个 Skill 对外暴露一个描述清单包含名称、用途说明、参数 Schema。Runner 维护一张工具注册表当模型说要调用某个工具时Runner 会先做语法层面的匹配和参数校验校验通过才真正执行。有一次我写 Skill 时把参数名从keyword改成了query结果模型连续三轮都在用旧参数名调用Runner 每轮都返回参数校验错误。排查了半天才发现是描述信息没有同步更新模型看到的注册表还是旧的。这个教训说明工具参数 Schema 一旦变更必须同步更新工具描述否则模型不会自动知道新参数。记忆层接口执行引擎的第三个连接对象是记忆模块。每一轮工具调用的产物比如读取的文件内容、查询到的记录不属于长期记忆只属于当前会话的短期上下文只有用户明确要求记住或者到达某个重要节点时Runner 才会调用记忆接口做持久化。理解这一点你才能解释为什么很多 Agent 在会话中表现很好但新开一个会话就失忆——因为执行引擎默认只做短期上下文管理长期记忆需要显式触发。2.3 会话槽位与串并行调度pi-embedded-runner 里还有一个容易忽略但很重要的概念会话槽位slot。一个 Runner 实例可以同时管理多个 Agent 会话每个会话拥有独立的上下文缓冲区和状态栈。这有点像数据库连接池——每个连接都是隔离的互不干扰。由于底层模型调用通常是串行的尤其本地模型Runner 需要一个调度策略来决定多个会话之间如何抢占模型资源。我跑下来的感受是OpenClaw 默认更偏向一个主任务占住模型的调度方式并行能力有限。如果你在同一个进程里同时跑多个 Agent 任务可能会出现一个任务的长工具调用堵住了另一个任务的模型请求。解决方案通常是把不同任务拆到不同的 Runner 实例不同的 Node 进程而不是在同一个实例里硬塞高并发。下面是我整理的一份调度参数对照表帮助你在调优时有一个直观的参照参数作用建议值/做法maxIterations单个任务最多循环多少轮简单任务 8 轮复杂任务 15~20 轮maxTokens单轮模型输出最大 token 数根据模型上下文留足工具结果的空间slotCountRunner 同时可承载的会话数本地模型建议 1~2云端 API 可到 5timeout单次工具调用超时网络类 30s本地文件类 10stoolRetries工具失败重试次数幂等工具 2 次非幂等工具 0 次这张表的每一条都来自真实调参经历。比如timeout这条我开始没设置结果某个网络工具在目标站点无响应时直接挂起整个任务卡了快 10 分钟。后来给工具加上 30 秒超时Runner 会在超时后把错误信息返回给模型模型自己决定是重试还是换方案整个系统立刻灵活了很多。3. 核心循环深入从意图到动作的每一步3.1 意图识别与任务分解LLM 如何输出结构化指令执行引擎的第一道工序是把自然语言目标变成可执行的指令序列。这一步通常不在 Runner 里而是在模型层但 Runner 必须能正确解读模型的输出。现在主流的做法有两种一种是让模型输出function_call格式的原生工具调用另一种是让模型输出严格的 JSON再由 Runner 里的解析器读取。我的经验是不要过度相信模型会严格遵守 JSON 格式。哪怕是表现很好的模型在长上下文和工具结果干扰下也可能输出多余的解释文字。我处理过一个很典型的情况模型明明应该输出{tool: read_file, params: {path: xxx}}结果在 JSON 前面加了一句好的我来帮你读取这个文件如果 Runner 没有做从文本中提取 JSON 片段的兜底这个调用就直接失败了。所以一个健壮的执行引擎必须内置容错解析先尝试严格解析失败后再做提取再失败才把错误返回给模型。3.2 工具调度的决策规则选哪个工具、传什么参数当模型提出多个可能的工具调用时Runner 不能全盘照收它需要基于规则做决策。这里主要看三件事工具是否在当前注册表中、参数是否符合 Schema、是否有足够的执行权限。工具描述的质量会直接决定模型的选择准确性。你在写 Skill 时描述不能只写读取文件要写清楚这个工具适合什么场景、有哪些边界、参数的含义。读取文件内容支持普通 UTF-8 文本文件不适合二进制文件返回文件前 200 行——这种描述能极大减少模型误调用的概率。参数校验是另一道关键防线。模型可能从上下文里提取了一个并不存在的文件名或者把数字类型的参数传成了字符串。Runner 在调用真实工具之前做一次 Schema 校验能拦截大部分低级错误。有一次模型连续三次调我的analyze_logs工具参数里传的次数都是负数如果 Runner 不拦截工具就会返回一堆没意义的统计拦截之后把校验错误给模型模型自己就意识到传参数错了重新传了正数。3.3 结果反馈与上下文更新失败信号如何进入下一轮工具执行完之后返回结果不会直接丢给用户它会先进入 Runner 的结果处理器。处理器要做三件事把结果格式化成紧凑的消息、把结果注入上下文、判断结果是否包含失败信号。结果格式化这一点特别重要。工具可能返回一个巨大的 JSON全量塞进上下文会浪费 token 占用。Runner 通常会做截断或摘要只保留前 N 个字符。我在做日志分析类 Skill 时工具动辄返回上万行日志一开始全量回填上下文很快就爆了后来在工具侧先做了聚合统计只返回 Top 10 错误和统计汇总模型处理起来又快又准。失败信号的处理同样关键。工具调用失败超时、权限不足、参数错误不能简单结束任务Runner 要把失败原因转换成模型可理解的自然语言放进上下文让模型自己决定下一步。比如文件读取失败路径不存在这个信号模型读完会尝试列出目录看看有哪些文件存在而不是直接放弃。这种失败即反馈的机制是 Agent 能自我纠错的核心。3.4 迭代刹车的安全边界设置没有刹车的 Agent 是危险的。如果模型在一个错误分支上打转或者工具不停地返回同样的结果任务可能会无限循环。pi-embedded-runner 提供了多层刹车机制。第一层是maxIterations也就是最多允许的思考-行动轮数。我一般给普通任务设 10 轮复杂任务 20 轮。超过这个数 Runner 会强制终止并返回已达到最大迭代次数的提示。第二层是相邻轮次的相似度检测如果连续两轮模型的工具调用意图完全相同Runner 会给模型一个警告你刚刚已经做过同样操作请确认是否需要继续。第三层是 token 预算上下文接近模型上限时 Runner 会主动触发滑窗把最旧的消息摘要掉而不是盲目扩充。这里我想多说一句安全边界本质上是给模型一次认错机会的设计。你不需要把每一条路径都堵死只需要在失控时软性打断然后让模型意识到自己在重复。实测下来相似度检测这一条能解决八成左右的循环问题比硬性中断体验好很多。4. 实战环境准备与第一个多步任务跑通4.1 处理 WSL 环境异常与准备 Node 运行时我最初是在 Windows 上尝试部署 OpenClaw 的结果启动阶段就遇到了社区里出现频率很高的那个提示openclaw 无法安全验证 wsl 环境。请在 powershell 中运行 wsl -- status。上网一查遇到的人不少这个提示的意思是系统还没有正确启用 WSL 2或者默认版本不对。排查思路不复杂。先打开 PowerShell执行wsl --status看返回的信息里 WSL 版本是不是 2。如果系统提示适用于 Linux 的 Windows 子系统未安装就需要先安装wsl --install。安装完成后重启再执行wsl --set-default-version 2确保用的是 WSL 2而不是旧版的 WSL 1。还有一个常见问题是在 BIOS 里未开启虚拟化功能这个用systeminfo检查虚拟化: 已启用就能确认。OpenClaw 本身的安装路径我建议直接走 Node.js 生态。先到官网安装 LTS 版本的 Node.js我用的 20.x然后全局安装 OpenClaw 就可以。这里有一个比较隐蔽的坑安装完 Node 之后要重开终端否则 npm 全局路径不会加载。我第一次就是没重开终端直接执行命令行提示找不到命令折腾了好一会儿。4.2 配置模型通道本地 Ollama 还是云端 API执行引擎本身不带模型它需要连接一个能输出工具调用意图的模型后端。两种主流方式我都试过。本地模型社区里很多人尝试用 Ollama 跑 qwen2.5-3b 关联到 OpenClaw。好处是免费、本地数据不出机器坏处是小模型的工具调用能力比较弱经常不按 JSON 格式输出。我在跑通之前做了两个优化一是把温度调到 0 或接近 0减少随机性二是在配置里给模型填了详细的工具调用示例。这里要说句公道话3b 这种小模型做简单问答可以做复杂的多步任务真的比较吃力建议至少 7b 起步有条件直接 14b。云端 API如果你配置了 OpenAI 兼容接口支持 OpenAI、或国内可直连的兼容服务Runner 连接会顺利很多。云端模型对工具调用的原生支持更好跑复杂任务的稳定性明显高于本地小模型。缺点是需要考虑 API 费用和隐私问题。我的建议是开发调试阶段用云端 API 把逻辑跑通之后换成本地模型做隐私敏感的任务两者配合最舒服。配置模型通道时核心是把模型的输入输出格式对齐 Runner 的预期。我贴一段我使用的配置思路以 Ollama 为例{ llm: { provider: ollama, baseUrl: http://localhost:11434, model: qwen2.5:7b, temperature: 0, maxTokens: 4096 }, runner: { maxIterations: 15, toolTimeout: 30000, toolRetries: 1 } }你不需要照抄这段重点是注意temperature和maxTokens这两个值。温度太高模型就爱自由发挥格式容易乱maxTokens太低会导致模型输出到一半就被截断工具调用意图不完整。4.3 编写一个自定义 Skill 并注册到 Runner光会配置还不够真正让执行引擎活起来的是自定义 Skill。我以读取本地文档并提取配置项为例演示一个最简 Skill 的写法。在 OpenClaw 里一个 Skill 通常包含一个描述文件和一个执行函数执行函数用 JavaScript 写Runner 通过描述文件知道这个工具的名称和参数要求。// skill: doc-config-extractor.js export default async function extractConfigItems({ filePath, keyword }) { const fs await import(node:fs); const content await fs.promises.readFile(filePath, utf-8); const lines content.split(\n); const results lines .filter(line line.includes(keyword)) .slice(0, 50); return { lineCount: results.length, matchedLines: results, }; }对应的描述信息需要为这个 Skill 写明名称、用途、参数 Schema。描述越具体模型越不容易误用{ name: extract_config_items, description: 读取文本文件中包含指定关键字的行适合从配置文档、日志、代码文件中提取配置项。不适合二进制文件。返回匹配行列表与前 50 行。, parameters: { type: object, properties: { filePath: { type: string, description: 文件绝对路径或相对路径 }, keyword: { type: string, description: 搜索关键字大小写敏感 } }, required: [filePath, keyword] } }把这两个文件放到 OpenClaw 的 skills 目录后重启 Runner执行引擎就会自动把新 Skill 注册进工具表。验证注册成功的方法是直接给 Agent 发一条使用该工具的任务然后看 Runner 的日志里是否有该 Skill 的加载记录。4.4 跑通检索整理输出的三步任务环境就绪后我做的第一个完整测试是一个三步任务读取项目文档里所有包含API_KEY的行去重后按文件名分组生成一个汇总表格。Agent 的实际执行路径可能和我预想的不完全一样但大体会是先列目录 → 读取文档 → 调用extract_config_items→ 汇总 → 生成表格。整个过程经历了大概 6 轮思考-行动循环每轮我都盯着 Runner 日志看确认工具调用意图被正确解析和执行。第一次跑的时候并没有一次成功。问题出在模型调用了extract_config_items之后返回结果里有 200 多行匹配数据我把这些全塞进了下一轮上下文模型很快就晕了最后的表格残缺不全。后来我给工具返回结果加了截断只保留前 30 行匹配并且在工具侧先做了简单的去重统计问题才消失。这个例子再次印证了我在第 3 章说的工具返回结果必须在进入上下文之前做瘦身否则模型再强也处理不了海量明细。5. 运行中的坑与调优工具返回格式、上下文与超时5.1 工具返回值不规范导致的LLM 发疯这是执行引擎实战里我遇到最多的一类问题。工具函数为了自己方便返回的是无格式的纯文本、或者嵌套很深的 JSON模型拿到这种结果后很容易产生幻觉开始脑补不存在的信息。一个非常典型的场景我写了一个查询服务器状态的工具返回值里有一段纯文本日志连接正常 200 OK。结果模型下一轮直接宣称服务器连接完全正常延迟 5ms丢包率 0%。可我的工具根本没返回延迟和丢包率。这就是典型的模型脑补——它根据上下文风格顺嘴编了细节。解决方案分两层。工具侧返回值尽量结构化用简单扁平的 JSONkey 命名清晰并明确注明以下数据为原始结果未加工Runner 侧在工具描述里加一条约定工具返回的是原始数据只能基于返回字段做分析不得推断字段之外的信息。加了这条约束后模型脑补的现象明显减少了但说实话无法百分百消除所以关键数据你一定要在工具侧做好验证和统计不要指望模型忠实转述。5.2 上下文窗口逼近限制时的处理策略执行引擎是个上下文贪吃鬼每轮模型输出、每轮工具结果、每轮状态记录都在累积 token。当你的任务超过 10 轮、工具又返回大段数据时上下文很容易逼近模型窗口上限。我在 pi-embedded-runner 里验证过的策略有三个按优先级排序截断优先、摘要其次、滑窗兜底。工具返回结果在进入上下文前先截断这是最廉价也最有效的方案如果截断后仍接近上限Runner 会把最早几轮思考-行动记录进行摘要压缩换成一行简短的第 1 轮完成了文件列表获取最后一道是滑窗直接把最早的对话内容丢弃。三种策略的取舍值得注意。截断有丢失信息的风险但工具结果的重复度通常不高丢失尾部明细影响较小摘要有信息失真风险适合处理历史过程而不是关键数据滑窗则会让模型失去记忆导致后半程任务可能遗漏早期的约束条件。我的建议是尽量让每个工具在源头就返回精简结果别依赖 Runner 做售后处理。5.3 超时、重试与幂等设计执行引擎在真实环境中工具调用失败是常态不是异常。网络抖动、文件锁定、外部服务无响应任何一项都能让工具调用失败。Runner 如何处理失败直接决定了 Agent 的稳定性。超时设置是第一道防线。不给超时的工具调用是定时炸弹。我给网络类工具统一设 30 秒超时本地文件类工具 10 秒超过就抛异常并把异常转成自然语言错误返回给模型。重试策略要区分工具是否幂等。幂等工具只读操作可以重试非幂等工具写操作、发消息重试要格外小心。有一次我写的通知类 Skill 没做幂等控制Runner 自动重试了两次结果用户收到了三条重复通知。后来在代码里加了请求去重 ID才解决问题。5.4 观察轨迹如何用日志审查 Agent 的思考过程执行引擎比纯聊天模型强的地方就是它可以复盘。Runner 只要开启轨迹日志每一轮的模型输出、工具调用意图、参数校验结果、工具返回摘要都会被记录下来。遇到 Agent 行为异常时你完全可以像回放监控录像一样看到它在哪一轮跑偏了。我强烈建议在调试阶段开启详细日志线上运行阶段至少保留错误级日志。有一次我的 Agent 莫名其妙总是漏掉任务里的一项要求怎么调 prompt 都没用后来翻轨迹日志发现模型在第三轮就已经忘记了原始目标自顾自地推进到了下一步。找到原因后我改了系统提示词要求模型在每轮开始前先复述一次原始目标——问题立即解决。这种问题没有轨迹日志基本不可能定位。6. 我的一点实操体会6.1 对执行引擎选型与配置的建议如果你准备在自己的项目里接 OpenClaw我的建议是先别急着堆功能花一天时间把pi-embedded-runner的配置和日志机制摸清楚。这个时间绝对值得因为后面你调试任何 Skill、处理任何Agent 不听话的问题都会回到执行引擎这个层面。具体来说一是把工具返回值瘦身变成你写 Skill 的默认习惯不要指望模型和 Runner 处理你随手丢回去的海量数据二是重视工具描述的质量你花在写描述上的时间会在模型调用准确率上成倍赚回来三是建立轨迹日志的复盘习惯Agent 不是黑盒它每一步都有痕迹善用痕迹能省掉大量猜谜时间。6.2 这个执行引擎未来还能怎么扩展往大了说pi-embedded-runner 这种嵌入式 Runner 的设计思路很适合往多 Agent 协作方向延伸。每个 Runner 是一个迷你执行单元多个 Runner 之间可以通过消息传递协作——这正好呼应了社区里讨论的 agents anywhere 和多 AI 协作的方向。我个人的下一步计划是尝试在同一个进程里跑多个 Runner 实例分别负责规划者和执行者角色并探索更精细的权限隔离方案。如果你想更进一步还可以研究怎么把 Runner 的事件流接入外部监控系统做可视化回放。无论如何执行引擎这个层面你理解得越深玩 Agent 的上限就越高。