OpenClaw 意图理解机制解析

一、总体思路:不做分类器,让 LLM 自己判断 OpenClaw 的意图理解与传统 NLU 系统(基于规则或意图分类器)有本质区别。它不在 LLM 之前设置一个独立的意图分类步骤,而是采用「能力驱动架构」: 通过多层前置过滤器处理结构化指令(斜杠命令、内联指令) 将丰富的上下文(工具列表、技能描述、项目文件)组装进系统提示词 让 LLM 自行判断用户意图并选择行动路径 核心逻辑:与其花力气训练一个分类器去猜意图,不如把"能做什么"告诉模型,让模型自己决定"该做什么"。这个选择的代价和边界,我们放到第五章再展开。 二、架构全景:四层管线 用户消息从进入到被理解,经过四层递进的处理管线。前三层是确定性的预处理,第四层才交给 LLM。记忆系统作为横切模块,为第四层的系统提示词组装提供上下文补充。 flowchart TD A[用户发送消息] --> B[第1层 路由层] B -->|选定 Agent| C[第2层 命令检测层] C -->|是控制命令| C1[直接执行,不进 LLM] C -->|都不是| D[第3层 指令解析层] D -->|提取内联指令 + cleaned 文本| IA[Inline Actions 阶段] IA -->|是技能命令| D1[路由到技能处理器] IA -->|不是技能命令| E[第4层 系统提示词组装] M[(记忆系统)] -.->|身份文件/MEMORY.md 注入| E M -.->|memory_search/memory_get 按需拉取| F E --> F[LLM 决策层] F -->|扫描技能索引| F1[read SKILL.md → 按指令执行] F -->|选择工具| F2[调用 exec / web_search / message 等] F -->|复杂任务| F3[spawn 子 Agent] 层 职责 技术手段 是否经过 LLM 第 1 层 决定由哪个 Agent 处理 绑定规则优先级匹配 否 第 2 层 拦截斜杠命令 命令别名表匹配(正则仅用于粗判 token) 否 第 3 层 剥离内联指令,输出 cleaned 文本 正则管线 否 第 4 层 理解用户意图,决定行动 LLM + 系统提示词 + 记忆 是 下面分两章展开:第三章讲 Pre-LLM 层(1-3 层),第四章讲 LLM 层(第 4 层)。 三、Pre-LLM 层:确定性处理(第 1-3 层) 前三层的共同特点:不依赖 LLM,用确定性代码处理确定性的事情。 3.1 路由层 — 消息该发给哪个 Agent 核心文件:src/routing/resolve-route.ts 消息到达后,系统首先决定由哪个 Agent 处理。这不是意图分类,而是身份路由,按优先级匹配: 优先级 匹配方式 含义 1 binding.peer 私聊绑定到特定 Agent 2 binding.peer.parent 线程继承父消息的 Agent 3 binding.guild+roles Discord 服务器 + 角色匹配 4 binding.guild Discord 服务器级别匹配 5 binding.team Teams 团队绑定 6 binding.account 账户级别绑定 7 binding.channel 渠道级别绑定 8 default 兜底,使用默认 Agent 同一个 OpenClaw 实例可以运行多个不同的 Agent,每个 Agent 有自己的人格(SOUL.md)、工具集和技能。路由层确保消息到达正确的 Agent,之后的意图理解才有意义。 3.2 命令检测层 — 拦截结构化指令 核心文件:src/auto-reply/command-detection.ts 系统通过命令别名表(src/auto-reply/commands-registry.ts)做确定性匹配,检测消息是否为已注册的控制命令。正则只用于粗判"是否可能包含命令 token",服务于权限计算,不参与命令识别本身: 1 2 3 4 // 别名表匹配:检测是否为已注册命令 hasControlCommand(text) → boolean // 基于命令别名表 // 正则粗判:仅用于权限计算 hasInlineCommandTokens(text) → boolean // /[a-z] 或 ![a-z] 开头(仅用于粗判) 匹配到的命令不需要 LLM 参与,直接执行: /reset → 清空会话,创建新 session /status → 返回当前会话状态 /model gpt-4 → 切换模型 设计原则:确定性意图用确定性方式处理,不浪费 LLM 的推理能力。 需要注意的是,技能命令(/skillname 或 /skill <name> 格式)虽然也是 Pre-LLM 处理,但不属于命令检测层,也不属于指令解析层。技能命令的解析发生在指令解析之后、进入 LLM 之前的 inline actions 阶段(src/auto-reply/reply/get-reply-inline-actions.ts),系统从多个目录(extra → bundled → managed → agents personal → project → workspace)按优先级合并加载可用技能,匹配后直接路由到对应技能处理器。 3.3 指令解析层 — 提取内联参数 核心文件:src/auto-reply/reply/directive-handling.parse.ts、src/auto-reply/reply/directives.ts 如果消息不是控制命令,系统会检查其中是否夹带了内联指令: 1 2 3 @claude-opus 帮我重构这段代码 → 模型选择指令 /thinking high 分析这个 bug → 思考深度指令 /elevated on 执行部署脚本 → 权限提升指令 与第 2 层的区别:第 2 层判断消息整体是不是命令(是则直接执行,不进 LLM);第 3 层从消息中剥离内联指令,剩余文本继续送给 LLM。 (1)不是槽位抽取,是正则管线剥离 这套机制不是传统 NLU 的槽位模式(先分类意图,再提取参数),而是无意图分类的正则管线——依次扫描、发现就剥离、剩下的送 LLM。 (2)管线实现 parseInlineDirectives 函数依次调用 8 个提取器: flowchart LR A[原始消息] --> B[extractThinkDirective] B --> C[extractVerboseDirective] C --> D[extractReasoningDirective] D --> E[extractElevatedDirective] E --> F[extractExecDirective] F --> G[extractStatusDirective] G --> H[extractModelDirective] H --> I[extractQueueDirective] I --> J[cleaned 文本 → 送给 LLM]每个提取器核心逻辑一样(directives.ts:21-49): 1 2 3 // 正则匹配 /指令名 + 可选级别参数 const match = body.match(/(?:^|\s)\/(?:thinking|think|t)(?=$|\s|:)/i); // 匹配到后:提取参数 → normalize 校验 → 从原文删除 → 返回 cleaned 文本 实际效果: 1 2 3 4 5 6 7 输入: "帮我分析这段代码 /thinking high @claude-opus" ↓ extractThinkDirective → 提取 thinking=high extractModelDirective → 提取 model=claude-opus ↓ 输出: cleaned = "帮我分析这段代码" ← 这才送给 LLM 配置: { thinkLevel: "high", model: "claude-opus" } (3)8 类内联指令 全部硬编码在代码里,不可通过配置扩展。除了这 8 类 directives,系统还会单独识别少量内联简命令(如 /help、/commands、/whoami、/id 等),这些在 reply-inline.ts 中处理,不走 directive 管线: 指令 别名 参数值 作用 /thinking /think, /t high / medium / low / off 控制 LLM 思考深度 /verbose /v on / off 控制工具输出详细程度 /reasoning /reason on / off / stream 控制扩展推理 /elevated /elev on / off / ask / full 控制执行权限级别 /exec - host / security / ask / node 控制命令执行参数 /status - 无参数 查询会话状态(可内联使用) /model @别名 模型名或配置别名 切换模型 /queue - mode / debounce / cap / drop 控制消息队列行为 (4)权限控制 通过 commandAuthorized 标志控制:授权用户所有指令生效;未授权用户的指令-only 消息(只有指令没有正文)会被直接丢弃,含正文时部分指令可能被忽略或持久化(非敏感项),敏感指令不会生效。elevated 在不可用时会给出 unavailable 提示。群组场景中,未 @提及 bot 时 elevated 和 exec 指令也会被忽略(防误触发)。 3.4 Pre-LLM 层小结 三层协同完成一件事:把确定性的控制参数(模型、思考深度、权限)用正则处理掉,只把模糊的用户意图(到底要做什么事)留给 LLM。LLM 不需要浪费推理能力去理解 /thinking high,只需要专注于"帮我分析这段代码"这个真正的用户意图。 四、LLM 层:意图理解的核心阵地(第 4 层) 经过 Pre-LLM 三层处理后,cleaned 文本进入第 4 层。这一层做两件事:组装 LLM 能看到的世界,然后让 LLM 自主决策。 ...

February 16, 2026 · Estimated Reading Time: 9min · Plutoxx28

研究了 OpenClaw 的记忆系统,发现它其实就做了一件事

你可能遇到过这种情况:上周跟 AI 助手聊了半天项目方案,这周再问“当时怎么定的?”,它一脸茫然。这不是它笨。LLM 的“记忆”本质上是上下文窗口(Context)——模型在一次请求里能看到的全部内容,由系统提示、对话历史、工具输出和你刚发的消息拼起来。模型的推理和回答只能基于 Context 里有的东西,但 Context 是短暂的、有上限的、越大越贵。你希望它像硬盘一样长期存储,但它的工作方式决定了它做不到。 最近在研究 OpenClaw,发现它的记忆系统解决这个问题的思路特别清晰,核心就一句话: 别指望模型在上下文里永远记得。把重要的东西写到文件里,需要的时候用检索找回来。 听起来朴素,但它把“写什么、怎么写、怎么找、快撑爆了怎么办”这一整套流程设计得很完整。先从“记忆长什么样”开始:它不是某个黑盒功能,而是一套能落盘、能检索、能在长对话里自我维护的工作流。 一、记忆长什么样:两本笔记 + 一组身份文件 1、记忆就是 Markdown 文件 OpenClaw 里的 Memory(记忆)是落在磁盘上的持久化内容,跟 Context 是两回事——Context 随对话结束就没了,Memory 不会。但模型不会自动“看到”Memory 里的内容,需要通过检索工具把相关片段拉回到 Context 里才能用。 具体形态上,OpenClaw 的记忆不是神秘数据库,就是工作区里的 Markdown 文件。官方文档原话: files are the source of truth; the model only “remembers” what gets written to disk. 它分两层,像两本笔记本: memory/YYYY-MM-DD.md —— 流水账。按天记录,追加写入,适合“今天发生了什么”“这周在推什么”。默认模板会要求 agent 在 session start 主动阅读今天和昨天的日记;同时索引器也会扫描 memory/ 目录,用于后续检索建库。 MEMORY.md(可选,不会自动创建,需要你主动建或让 agent 写入) —— 精华本。放长期偏好、关键决策、稳定不变的事实。但有个重要细节:它只在私聊 session 中注入,群聊/频道里不会加载,避免私人记忆泄露到公共对话。 2、每次对话都注入的“身份文件” 记忆文件解决的是“跨 session 记住发生过什么”,但 Agent 还需要知道“我是谁、用户是谁、该怎么做事”。OpenClaw 在每次 session 开始时会自动注入一组工作区文件到系统提示里,它们和记忆文件一起,构成了 Context 里跨 session 持续存在的部分: AGENTS.md —— 行为规则、协作约定 SOUL.md —— 人格、语气、风格 TOOLS.md —— 工具使用说明 IDENTITY.md —— 名字、角色 USER.md —— 用户画像、偏好 HEARTBEAT.md —— 定时任务(heartbeat)指令 BOOTSTRAP.md —— 首次运行的初始化脚本(只在全新工作区创建;按模板约定,完成后应删除,避免后续重复注入) 空文件会跳过,过大的文件会被截断。截断策略是保留 70% 头部 + 20% 尾部 + 中间插截断标记。这么做是因为文件开头通常是规则定义(最重要),末尾通常是最新追加的内容(次重要),中间可以牺牲。 还有个设计值得注意:当 OpenClaw 创建子 Agent 处理子任务时,只注入 AGENTS.md 和 TOOLS.md,其他文件全部跳过。子任务不需要完整人设,精简注入省 token。到这里我们解决了两个问题:“记忆放在哪”、“每次请求会带哪些长期设定”。下一步就该问:这些东西是怎么被写进去的? 二、“记住”这件事怎么发生:写入从哪来 OpenClaw 不会凭空产生记忆,它的“长期记住”来自写文件。写入来源通常有三种: 你明确说“记住这个”。助手就写进 MEMORY.md 或当天的 memory/YYYY-MM-DD.md。官方建议是:如果你想让某件事被记住,直接让 bot 写到 memory 里。 助手自己判断“这值得记”。比如你在对话中确认了一个关键配置、一个设计决策,助手可能主动落盘。 系统在关键时刻提醒它“现在该存档了”。这是后面要讲的 Memory Flush。 这里的关键不是“OpenClaw 记忆很强”,而是它把行为设计成了:能写就写,写完可检索。这比“在对话里口头说过一次”靠谱得多。写进文件只是第一步。下一步才是关键:当你换一种问法,它还能不能把那段内容找回来? 三、写完怎么找回来:索引 + 混合检索 有一个前提需要先说清楚:OpenClaw 的检索不是自动发生的。memory_search 是一个工具(tool),模型需要根据对话上下文主动判断“这个话题我可能记过”,然后决定调用它。这跟传统 RAG 的“每次请求都自动塞入相关片段”不同——OpenClaw 不会在每次对话中都注入无关的记忆内容,该查的时候才查,省 token。 理解了这一点,再看索引和检索的设计就更清楚了。如果只有一堆 Markdown 文件,没有索引,你会遇到两个问题:文件越来越多,不知道在哪天哪段;你的问法一变,它就找不到——你问“那个数据库连接串”,文件里写的是 POSTGRES_URL。 所以 OpenClaw 在写文件之外,还做了索引:监听文件变更 → 切片 → 做 embedding → 建本地 SQLite 索引 → 检索时混合打分。 1、文件监听:不是改了就重建 OpenClaw 用 chokidar 盯着 MEMORY.md、memory/ 和配置的额外路径。变更后有 1.5 秒的 debounce(等文件写稳定),同步时用 SHA-256 对文件做 hash,内容没变就跳过,不重新切片。如果 embedding 模型或切片参数变了,会触发全量重建索引。重建时用临时数据库做完所有工作再原子交换过来,中途失败不影响原库。 2、切片:400 tokens 一段,80 tokens 重叠 Markdown 按行累积切片,每段大约 400 tokens,相邻段有 80 tokens 重叠。重叠是为了防止一条“决策”跨段落边界,检索只拿到一半。 3、混合检索:向量 + BM25 关键词 这是我觉得设计最实用的部分。OpenClaw 同时用两种检索: 向量检索擅长找“意思相近”的内容。你问“数据库连接”,它能匹配到 POSTGRES_URL、connection string、DB url 这些不同说法。 BM25 关键词检索(一种经典的全文检索算法,按关键词的出现频率和稀有程度打分)擅长找“字面命中”的内容。人名、日期、ID、配置键、代码符号这些,向量反而不如关键词靠谱。 合并方式是:两边各取一个候选池(默认是最终结果数的 4 倍),按 chunk ID 做并集合并,然后加权打分。默认权重是 0.7 × 向量 + 0.3 × 关键词,低于 0.35 分的结果过滤掉,最终返回前 6 个。官方文档自己也说了,这不是“IR 理论上最完美的方案”,但简单、快、在实际笔记场景下效果不错。 补充一点:默认情况下只有 memory 文件会被索引,但 OpenClaw 还有一个实验性功能——Session 转录索引。要用它,开启后(experimental.sessionMemory = true,还需要把 sources 里包含 "sessions",如 sources: ["memory", "sessions"]),这样即使你没让助手“记住”某件事,只要对话中讨论过,memory_search 也有可能通过语义检索找到那段对话。相当于把检索范围从“主动写下的笔记”扩大到了“所有聊过的内容”。 到这里,“写”和“找”都打通了。但还有一个现实挑战:对话越长,上下文越容易被撑爆。OpenClaw 怎么处理这个? 四、长对话怎么不炸:三个机制 把 Agent 当长期伙伴用,对话一定会变长。OpenClaw 用三层策略来治理上下文膨胀。 1、Compaction:把早期历史压成摘要 当 session 的上下文快到模型窗口上限时,OpenClaw 触发 compaction:把较早的历史总结成一条摘要,保留最近的消息。 好处直接:token 用量拉下来,会话能继续。但副作用也直接:摘要是有损的,细节会被压缩掉。比如一段很长的对话经过 Compaction 后可能变成: 1 2 3 [Compaction 摘要] 用户和助手讨论了数据库迁移方案,从 PG-A 迁到 PG-B,确认了连接串。 讨论了是否使用 GraphQL,最终决定不用。还调试了一个 CORS 配置问题并解决。 信息是对的,但具体的连接串是什么、不用 GraphQL 的理由是什么、CORS 怎么配的——这些细节全丢了。 2、Memory Flush:压缩之前先存档 这是我觉得 OpenClaw 最聪明的设计。 它不指望 compaction 做到无损,而是在 compaction 真正发生之前,插入一个静默的 agentic turn,让模型自己判断"哪些具体事实值得留下来",然后以原始粒度写入文件。注意,Flush 不是在做摘要,而是在做"挑拣+落盘"——模型会把具体的值、具体的理由、具体的结论写下来。 同样的对话,Flush 写入 memory/2026-02-11.md 的内容可能是: 1 2 3 4 ## 数据库迁移 - 从 PG-A 迁移到 PG-B,新连接串:POSTGRES_URL=postgresql://admin:xxx@pg-b.internal:5432/prod - 决定不用 GraphQL,理由:现有 REST API 已覆盖所有场景,团队没有 GraphQL 经验,引入会增加维护成本 - CORS 问题:nginx 配置里 Access-Control-Allow-Origin 要用具体域名,不能用通配符 *(生产环境安全要求) 区别一目了然:Compaction 压缩的是对话历史,丢细节换空间;Flush 保存的是具体事实,后续可以被精确检索回来。 具体来说,系统会同时注入两条提示:一条追加到 system prompt,告诉模型当前 session 即将压缩,应该把值得长期保存的内容写到磁盘;一条作为 user prompt,指示模型将笔记写入 memory/YYYY-MM-DD.md,如果没什么需要保存的就回复 NO_REPLY。 触发时机是 contextWindow - reserveTokensFloor(20000) - softThresholdTokens(4000)。比如 200K 上下文的模型,大约在 176K tokens 时就会触发 flush,比真正的 compaction 提前了 4000 token 的量。每个 compaction 周期只 flush 一次,防止重复。用户完全感知不到这个过程。 为什么这步很关键? 因为它把风险从"摘要是否写对"转成了"有没有在压缩前把关键点落盘"。一旦落盘,哪怕摘要漏了,后续通过 memory_search 还能检索回来。一句话:先存档,再压缩。 3、Session Pruning:专治工具输出太肥 Pruning 解决的是另一类膨胀:工具输出。Agent 在工作过程中会调用各种工具(执行命令、读文件、搜索等),这些工具的返回结果(toolResult)会原样留在上下文里。一个 exec 跑出几万行日志、一个 read 拉回一整个配置文件,堆积起来会快速耗尽 token。 Pruning 的策略很克制:只修剪 toolResult,不动用户和助手的正常消息;而且只影响这次发给模型的上下文,不回写磁盘上的对话记录。分两级:Soft-trim——保留头部和尾部,中间用省略号替代。适用于内容虽大但可能有用的输出: 1 2 3 4 5 6 7 8 9 10 11 12 13 # 修剪前(原始 toolResult,假设 3000 行日志) [2026-02-11 10:01:03] Starting migration... [2026-02-11 10:01:03] Connecting to source DB... [2026-02-11 10:01:04] Reading table users (150,000 rows)... [2026-02-11 10:01:05] Reading table orders (2,300,000 rows)... ...(中间 2990 行省略)... [2026-02-11 10:15:22] Migration completed. 45 tables, 0 errors. # Soft-trim 后 [2026-02-11 10:01:03] Starting migration... [2026-02-11 10:01:03] Connecting to source DB... ... [2026-02-11 10:15:22] Migration completed. 45 tables, 0 errors. Hard-clear——整个替换成一句提示。适用于更老的、大概率不再需要的输出: 1 2 # Hard-clear 后 [Old tool result content cleared] 可以理解成:不删档案,只是这次开会别把 5 万行日志打印出来。 现在把这些机制串起来,才是完整的"记忆系统"长什么样。 五、一次完整流程长什么样 先看整体数据流: graph LR A[用户对话] -->|"记住这个"| B[写入 memory/*.md] B -->|chokidar 监听| C[切片 + Embedding] C --> D[(SQLite 索引)] E[用户提问] -->|Agent 主动调用| F[memory_search] F --> D D -->|混合检索结果| G[注入 Context → 回答] H[Context 快满] -->|提前 4000 token| I[静默 Flush → 写入 memory] I --> B H -->|到达上限| J[Compaction 压缩摘要] 把上面的机制串起来,一个典型场景是这样的: 你: 把数据库从 A 迁到 B,连接串是 POSTGRES_URL=xxx,记一下。 助手: 好,写入 memory/2026-02-11.md。 → 文件变更 → 1.5s 稳定 → hash 比对 → 重新切片 + embedding → 更新索引 …聊了很多,跑了很多命令… (接近 flush 阈值) 系统 [静默]: Pre-compaction memory flush… ...

February 12, 2026 · Estimated Reading Time: 4min · Plutoxx28