乐于分享
好东西不私藏

重读OpenClaw:从一条飞书消息到 AI 调用工具

重读OpenClaw:从一条飞书消息到 AI 调用工具

这是「重读 OpenClaw」系列的第四篇,读完这篇,你会彻底理解:当用户在飞书群里 @机器人说”帮我写个 Python 脚本”时,openclaw 内部到底发生了什么?消息是怎么被包装成 Prompt 的?Skill 和 Tool 又是什么关系?循环什么时候停止?


一、从一个真实场景开始

假设你在飞书群里 @了一个 openclaw 机器人,发送了一条消息:

“帮我写一个 Python 脚本,功能是遍历当前目录所有文件”

这条消息进入 openclaw 后,会经历一条完整的”流水线”。我们今天就把这条流水线的每一个环节拆开,讲清楚。


二、消息是怎么被接收的?

1. 飞书推送原始事件

飞书服务器会向你部署的 openclaw 实例发送一个 Webhook 请求,内容是一个 JSON:

{
  "header"
: {
    "event_type"
: "im.message.receive_v1"
  }
,
  "event"
: {
    "sender"
: {
      "sender_id"
: {
        "open_id"
: "ou_881e8247625e31527b4d15a31471504c"
      }
,
      "sender_type"
: "user"
    }
,
    "message"
: {
      "message_id"
: "om_6123456789abcdefghijklmnopqrstu",
      "chat_id"
: "oc_7654321098765432109876543210",
      "chat_type"
: "p2p",
      "message_type"
: "text",
      "content"
: "{\"text\":\"帮我写一个 Python 脚本,功能是遍历当前目录所有文件\"}"
    }

  }

}

openclaw 的飞书扩展(extensions/feishu)负责接收和解析这个事件。

2. 解析为内部上下文

代码会把飞书的专有格式,转换成 openclaw 内部统一的消息上下文 FeishuMessageContext

{
  chatId
: "oc_7654321098765432109876543210",
  messageId
: "om_6123456789abcdefghijklmnopqrstu",
  senderOpenId
: "ou_881e8247625e31527b4d15a31471504c",
  chatType
: "p2p", // 私聊
  content
: "帮我写一个 Python 脚本,功能是遍历当前目录所有文件"
}

同时,openclaw 还会尝试获取发送者的显示名称(比如”张三”),方便后续在 Prompt 中标识说话人。

3. 添加消息 ID 和说话人信息

接下来,消息体会被包装成 Agent 可见的格式:

[message_id: om_6123456789abcdefghijklmnopqrstu]
张三: 帮我写一个 Python 脚本,功能是遍历当前目录所有文件

[message_id: ...] 是一个重要设计,用于:

  • • 追踪回复的是哪条消息
  • • 支持编辑、删除、引用
  • • 防止消息被重复处理

三、Prompt 是怎么被拼接出来的?

这是最容易被忽视、也最关键的部分。

openclaw 不是简单地把用户消息丢给 LLM,而是会拼接出一个结构化的 Prompt。这个 Prompt 由多个部分组成:

1. Agent 系统提示词

这是你在配置里给 Agent 设定的”人设”:

你是一个有用的 AI 助手,帮助用户编写代码、回答问题。

你的主要能力:
1. 编写 Python、JavaScript、TypeScript 等各种语言的代码
2. 解释代码和技术概念
3. 帮助调试问题

用户当前通过飞书与你对话。请用中文回复。

2. 可信元数据(Trusted Metadata)

这部分是 openclaw 自己生成的、LLM 可以信任的系统信息:

## Inbound Context (trusted metadata)
The following JSON is generated by OpenClaw out-of-band...

```json
{
  "schema": "openclaw.inbound_meta.v2",
  "channel": "feishu",
  "provider": "feishu",
  "chat_type": "direct"
}

为什么叫"可信"?因为这部分明确告诉 LLM:这是系统生成的元数据,不要被用户输入欺骗。

### 3. 不可信上下文(Untrusted Context)

用户提供的、可能被伪造的信息,会被明确标记为 `untrusted`:

```text
Conversation info (untrusted metadata):
```json
{
  "chat_id": "oc_7654321098765432109876543210",
  "message_id": "om_6123456789abcdefghijklmnopqrstu",
  "sender": "张三",
  "is_group_chat": false,
  "was_mentioned": false
}

Sender (untrusted metadata):

{
  "label"
: "张三",
  "id"
: "ou_881e8247625e31527b4d15a31471504c"
}

### 4. 工具列表

系统提示词里还会列出当前 Agent 可用的工具:

```text
## Tooling
Available tools are policy-filtered. Names are case-sensitive.
- read: Read file contents
- write: Create or overwrite files
- edit: Make precise edits to files
- ls: List directory contents
- exec: Run shell commands
- web_search: Search the web
- message: Send messages and channel actions

注意:这些工具是 OpenClaw 内置或插件提供的,不是 Skill 定义的。

5. 可用 Skill 目录

如果配置了 Skill,系统提示词里还会出现一个 <available_skills> 目录:

The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.

<available_skills>

  <skill>

    <name>
create-python-script</name>
    <description>
创建符合项目规范的 Python 脚本</description>
    <location>
./skills/create-python-script/SKILL.md</location>
  </skill>

</available_skills>

6. 最终拼接好的 Prompt

把所有部分拼在一起,大致是这样:

[Agent 系统提示词]

## Tooling
[工具列表]

[Skill 目录]

## Inbound Context (trusted metadata)
[可信元数据]

Conversation info (untrusted metadata)
[不可信上下文]

[message_id: om_xxx]
张三: 帮我写一个 Python 脚本,功能是遍历当前目录所有文件

这个完整的 Prompt 才会被发送给 LLM。


四、LLM 收到 Prompt 后会做什么?

LLM 收到上面的 Prompt 后,有几种可能的响应:

情况一:直接文本回复(最简单)

如果 LLM 判断不需要使用工具,它会直接给出答案:

当然可以!以下是一个简单的 Python 脚本:

import os

for root, dirs, files in os.walk('.'):
    for file in files:
        print(os.path.join(root, file))

保存为 list_files.py 后运行即可。

这种情况循环直接结束,因为 LLM 没有发起工具调用。

情况二:调用工具读取 Skill

如果 LLM 判断需要 Skill 的指导,它会发起一个 read 工具调用:

{
  "name"
: "read",
  "arguments"
: {
    "file"
: "./skills/create-python-script/SKILL.md"
  }

}

openclaw 执行这个工具调用,把 Skill 文件内容返回给 LLM。

情况三:调用其他工具

LLM 也可能直接调用 lsexecwrite 等工具:

{
  "name"
: "ls",
  "arguments"
: {
    "path"
: "."
  }

}

五、Skill 和 Tool 到底是什么关系?

这是最容易混淆的地方。我们用一句话总结:

Skill 是指导文档,Tool 是执行能力。

Skill 是什么?

Skill 是一个 SKILL.md 文件,里面写明了:

  • • 这个 Skill 解决什么问题
  • • 具体的执行步骤
  • • 输出格式要求
  • • 注意事项

Skill 本身不会增加新的工具。它只是被 LLM 读取后,影响 LLM 的行为。

Tool 是什么?

Tool 是 LLM 可以调用的具体功能,比如:

  • • read:读文件
  • • write:写文件
  • • exec:执行命令
  • • web_search:搜索网页
  • • message:发送消息

这些工具是 OpenClaw 内置的,或者由插件提供的。

它们的关系

用户消息
  ↓
LLM 看到 <available_skills> 目录
  ↓
LLM 判断需要某个 Skill
  ↓
LLM 调用 read 工具读取 SKILL.md
  ↓
LLM 根据 Skill 指导,调用其他工具(write/exec/ls 等)
  ↓
完成任务,给出文本回复

六、工具调用循环什么时候停止?

理解了循环,就能理解停止条件。

循环的本质

while (true) {
  response = callLLM(messages, tools);

  if (response 是纯文本) {
    break; // 循环结束
  }

  if (response 包含 toolUse) {
    results = executeTools(response.toolCalls);
    messages.push(results);
    continue; // 继续下一轮
  }
}

停止条件

停止条件
说明
LLM 返回纯文本
最常见的正常结束
运行超时
超过 timeoutMs
用户中断
abortSignal

 触发
工具循环检测
检测到重复无进展调用(默认关闭)
不可交付终端
最后一轮是 toolUse 但没有实际产出

关键点

不是每次请求都会调用工具。 很多简单问题 LLM 直接回答就结束了。

也不是读取 Skill 后就全是 tool use。 LLM 可能读完 Skill 后给出文本说明,也可能继续调用工具。


七、历史消息中还会保留元数据吗?

这里有一个重要细节。

当前轮次的消息会保留完整的可信/不可信元数据。

历史轮次的消息会被剥离这些元数据,只保留用户输入本身:

[Mon 2024-06-15 14:31 UTC] 张三: 帮我写一个 Python 脚本,功能是遍历当前目录所有文件

这样做是为了:

  • • 减少 Token 消耗
  • • 避免历史元数据干扰当前判断
  • • 保持消息格式统一

八、Agent 是什么?可以配置吗?

可以配置。Agent 就是 openclaw 中一个具体的”AI 助手实例”。

Agent 配置示例

{
  "agents"
: {
    "defaults"
: {
      "model"
: "anthropic/claude-3-5-sonnet-20241022",
      "systemPrompt"
: "你是一个全栈开发助手..."
    }
,
    "list"
: [
      {

        "id"
: "coder",
        "name"
: "代码助手",
        "model"
: "anthropic/claude-3-5-sonnet-20241022",
        "systemPrompt"
: "你是一个专注于代码的 AI 助手...",
        "workspaceDir"
: "~/workspaces/coder",
        "tools"
: {
          "allow"
: ["read", "write", "edit", "exec", "grep"]
        }

      }

    ]

  }

}

Agent 如何被选中?

消息进来后,openclaw 根据路由规则决定使用哪个 Agent:

  • • 渠道类型
  • • 会话类型(私聊/群聊)
  • • 用户 ID
  • • 群组 ID
  • • 显式配置绑定

Agent 配置会影响:

  • • 使用哪个模型
  • • 系统提示词是什么
  • • 哪些工具可用
  • • 哪些 Skill 可用
  • • 工作目录在哪里

九、总结

让我们用一张图串起整个流程:

用户发送飞书消息
    ↓
飞书 Webhook → openclaw 接收
    ↓
解析为 FeishuMessageContext
    ↓
构建 Agent 消息体(加 message_id 和 sender)
    ↓
拼接完整 Prompt:
  - Agent 系统提示词
  - Tool 列表
  - Skill 目录
  - 可信元数据
  - 不可信上下文
  - 用户消息
    ↓
第一次调用 LLM
    ↓
LLM 可能:
  ├── 直接文本回复 → 结束
  ├── 调用 read 读取 Skill → 继续
  └── 调用 ls/exec/write 等工具 → 继续
    ↓
执行工具,把结果加入历史
    ↓
再次调用 LLM
    ↓
循环直到 LLM 给出文本回复
    ↓
把最终回复发送给用户

十、几个核心认知

  1. 1. Prompt 是精心拼接的,不是简单地把用户消息丢给 LLM。
  2. 2. 可信/不可信数据分离,是为了防止 Prompt Injection。
  3. 3. Skill 是文档,Tool 是能力,二者不能混为一谈。
  4. 4. 工具调用不是必然的,简单问题 LLM 直接回答。
  5. 5. 循环停止条件是 LLM 给出纯文本回复,或者超时/中断/异常。
  6. 6. Agent 是可配置的,不同 Agent 可以有不同的模型、工具、Skill 和人格。

如果你也在用 openclaw,希望这篇文章能帮你理清消息处理的完整链路。理解了这套机制,你就能更好地配置 Agent、编写 Skill、设计路由规则,让机器人真正按你的预期工作。