首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一个 AI Agent 是怎么思考的——从 OpenClaw 源码看执行循环与工具调用

一个 AI Agent 是怎么思考的——从 OpenClaw 源码看执行循环与工具调用

作者头像
陆业聪
发布2026-04-02 12:53:44
发布2026-04-02 12:53:44
2760
举报

📰 科技要闻

• 字节跳动以413亿元出售沐瞳科技,AI业务高速扩张背景下,移动游戏业务的战略收缩意味深长。

• Andrej Karpathy 公开表示自去年12月起几乎不再亲自写代码,80%的编程工作已交给 AI Agent,他把自己现在的状态描述为"AI 痴狂"。

• Anthropic 发布 AI Agent 评测方法论,强调 Agent 的多轮交互特性使传统单轮评测完全失效,推荐从20-50个真实失败案例起步建立评估体系。

上一篇我们聊了 OpenClaw 的四层架构和心跳机制——一个 AI 助手是怎么"活着"的。但有几个更核心的问题没讲完:

LLM 是怎么知道该调用哪个工具的?工具调用结果怎么喂回去?Agent 是怎么决定"我想清楚了,可以回复了"?多个 Agent 并发运行时,消息怎么路由到正确的会话?

这些才是 Agent 架构里最有技术含量的部分。今天把它们逐一拆开。

一、Agent 执行循环:不是一问一答,而是一个 while 循环

很多人第一次接触 LLM 时,脑子里的模型是这样的:我发一条消息 → 模型返回一段文字 → 完事。

这是聊天模式,也是大多数人熟悉的。但 Agent 不是这样工作的。

OpenClaw 的 Agent 执行流程本质上是一个 ReAct 循环(Reason + Act),用伪代码描述就是:

代码语言:javascript
复制
// Agent 主循环(简化版)
while (true) {
// 1. 把消息历史喂给 LLM
response = await llm.complete(messages)// 2. 模型说"我要调工具"
if (response.hasToolCalls()) {
results = await executeTool(
response.toolCalls
)
// 把结果追加到消息历史
messages.push(toolResultMessage(results))
// 继续循环,让模型再想一轮
continue
}// 3. 模型说"我想清楚了,回复用户"
break
}

关键点:每次 LLM 返回工具调用请求,不是终点,而是循环的一轮。工具结果被追加进消息历史,下一轮 LLM 拿到的上下文就包含了这次的执行结果,再决定下一步。

用 Android 的类比来说:这就像 RecyclerViewonBindViewHolder 不是只跑一次,而是根据当前数据状态反复被调度,直到 adapter 认为"这个 item 渲染完了"为止。

循环什么时候终止?

OpenClaw 里有几个终止条件:

• 模型不再输出 tool_use 块,直接返回文本 → 正常结束

• 达到最大工具调用轮数(防止无限循环)→ 强制截断

• 某个工具执行报错,错误信息喂回模型后,模型选择放弃 → 带错误结束

• 上下文长度即将超限 → 触发上下文压缩或截断

最后这个值得单独说一下。上下文长度限制是 Agent 最容易踩的坑。一个复杂任务调了十几次工具,每次工具返回几百行输出,很快就把 128K token 的窗口塞满了。OpenClaw 用了动态上下文管理来处理这个问题,下面会讲到。

二、Tool Use 的协议:LLM 和工具之间怎么对话

工具调用听起来神秘,实际上协议非常简单。本质上是:LLM 输出一段结构化 JSON,运行时解析它、执行对应函数、把结果格式化成另一段 JSON 喂回去

Anthropic 的 Claude API 格式是这样的:

代码语言:javascript
复制
// LLM 输出(assistant 消息)
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01XFx",
"name": "exec",
"input": {
"command": "ls /root"
}
}
]
}// 执行完后追加的 user 消息
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01XFx",
"content": "workspace/ .openclaw/ ..."
}
]
}

注意 tool_use_id 这个字段——它把一次工具调用和它的结果对应起来。模型可以在一个回复里同时请求多个工具调用(并行工具调用),运行时需要正确地把每个结果对应回去。

工具描述怎么写才好用

OpenClaw 在 System Prompt 里给 LLM 注入了所有可用工具的 JSON Schema 描述。写得好不好,直接影响模型调用的准确率。

一个反面例子:

代码语言:javascript
复制
{
"name": "exec",
"description": "Run a command",
"parameters": {
"command": {
"type": "string"
}
}
}

一个好一点的例子:

代码语言:javascript
复制
{
"name": "exec",
"description": "Execute shell commands. Use yieldMs
to background long-running tasks.
Use pty=true for interactive CLIs.",
"parameters": {
"command": {
"type": "string",
"description": "Shell command to run"
},
"yieldMs": {
"type": "number",
"description": "ms before backgrounding"
},
"background": {
"type": "boolean",
"description": "Run immediately in bg"
}
}
}

description 里的信息量直接决定模型能不能在合适的时机调用合适的工具,参数的 description 也不能省。这其实就是 Prompt Engineering 在工具层面的体现——你在给模型写使用说明书

三、上下文管理:塞不进去怎么办

这是 Agent 开发里最容易被低估的问题。

假设一个会话已经跑了一个小时,消息历史有几十轮,中间执行了大量工具,每次 exec 返回的输出少则几百字、多则几千字。这些全部塞进 LLM 的上下文,不仅贵,而且当接近窗口上限时,模型的"记忆"会开始出问题——它会忘掉开头的 System Prompt,或者把早期的重要信息丢掉。

OpenClaw 的三层应对策略

第一层:工具输出截断。exec 的输出默认截断到 2000 行 / 50KB,超出的部分提示"已截断,可用 offset 参数继续读取"。这样模型知道有更多内容,可以主动分页,而不是被动丢失。

第二层:消息历史压缩。当上下文接近阈值时,OpenClaw 会把较早的消息轮次进行摘要压缩——把几轮对话、工具调用及结果,浓缩成一段"这段时间发生了什么"的摘要,以一条 assistant 消息的形式留在历史里。这和 Android 系统在内存紧张时 kill 掉后台进程、但保留 savedInstanceState 是同一个思路。

第三层:持久化记忆。有些信息不应该随着历史压缩消失,比如用户的名字、重要的约定、工作目录。OpenClaw 把这类内容写到 MEMORY.md 文件,并在每次会话初始化时注入到 System Prompt 里。这就是为什么可以跨会话记住你的偏好——不是模型记住了,是文件记住了,每次"念"给模型听。

这里有个容易混淆的点:LLM 本身没有跨会话记忆,它的"记忆"完全依赖于你喂给它的上下文。所以 Agent 框架做的大量工程工作,本质上都是在解一道题:如何在有限的 token 窗口里,装下对完成当前任务最有价值的信息?

四、会话路由:消息怎么找到正确的 Agent

OpenClaw 同时支持 25+ 个消息渠道(企业微信、Telegram、飞书、Discord……),每个渠道可以有多个用户或群组在同时对话。消息进来了,怎么知道该交给哪个会话处理?

答案是 Session Key

Session Key 的设计

每一个会话都有一个唯一标识,格式大致是:

代码语言:javascript
复制
agent:main:wecom:direct:yeconglu
agent:main:telegram:group:123456789
agent:main:discord:thread:987654321

包含了:agent 标识 + 渠道类型 + 会话类型(direct/group/thread)+ 目标 ID。

当一条新消息进来,Gateway 层做的第一件事是根据消息来源(渠道 + 发送者/群组 ID)查找或创建对应的 Session,然后把消息放进这个 Session 的队列。

用 Android 类比:这有点像 Intent 的路由机制。一个 Intent 带着 action 和 data,Android 系统根据已注册的 IntentFilter 找到对应的 Activity 或 Service 来处理。Session Key 就是那个 IntentFilter 的匹配规则。

并发安全:每个 Session 是单线程的

多个用户同时发消息,会不会出现竞态?

OpenClaw 的设计是:每个 Session 同一时刻只处理一条消息。新消息排队,等上一轮处理完再开始下一轮。不同 Session 之间完全并发,互不干扰。

这和 Android 主线程的设计哲学一样:主线程是单线程的,所有 UI 操作串行执行,用 Handler 排队,不需要加锁。你不需要在 UI 操作里担心并发安全,因为根本不会并发。代价是如果你在主线程做了耗时操作,整个 UI 就卡了——所以耗时操作要放到子线程(在 OpenClaw 里对应的是子 Agent)。

五、子 Agent:耗时任务的异步处理

前面说了,每个 Session 是单线程串行的。但有些任务天生就要跑很久——比如让 Agent 跑一个完整的代码审查、写一篇几千字的文章、执行一系列 shell 命令。

如果这些任务都阻塞主 Session,那用户发下一条消息就要一直等着。这显然不对。

解法是子 Agent(Subagent)

Subagent 的工作方式

主 Agent 调用 sessions_spawn 工具,创建一个独立的隔离会话,给它一个任务描述,然后 立即返回,不等结果。主 Session 就解放了,可以继续响应用户的其他消息。

子 Agent 在自己的 Session 里跑完任务,完成后通过 announce 机制把结果推送到目标渠道(比如企业微信群)。

比如,小红书发布任务可以配置成定时自动写:cron 任务触发 → 创建子 Agent → 子 Agent 独立跑完整个流程(抓素材 → 写文章 → 推送草稿箱 → 通知群)→ 完成后发结果到群里。整个过程主 Session 完全不参与。

再用 Android 类比一次:这非常像 WorkManager。你在主线程提交一个 WorkRequest,WorkManager 在后台调度执行,不阻塞主线程,完成后可以通过 LiveData 观察结果。子 Agent 就是 AI 版的 Worker。

ACP:让专业工具接管

OpenClaw 还支持另一种模式:ACP(Agent Communication Protocol),可以把任务交给专业的编码 Agent(比如 Claude Code 内网版、Codex)去执行。

区别在于:普通 subagent 用的是 OpenClaw 内置的通用 LLM 执行引擎;ACP agent 是完全独立的进程,有自己的工作目录、文件系统访问、甚至沙箱环境,专门针对特定场景优化(比如代码生成、代码 review)。

用 Android 类比:普通 subagent 是在同一个 App 里的后台 Service;ACP agent 是独立的应用进程,通过 AIDL 通信,完全隔离。

六、Skill 机制:给 Agent 装插件

OpenClaw 有一套 Skill 系统,本质上是 注入式的行为规范

每个 Skill 是一个目录,包含一个 SKILL.md 文件。里面写的是:什么情况下激活这个 skill、激活后 Agent 应该遵循哪些步骤、有哪些注意事项。

Skill 的工作流程:

• System Prompt 里列出所有可用 Skill 的名称和 description

• Agent 处理用户请求时,判断是否匹配某个 Skill

• 匹配到了,用 read 工具读取 SKILL.md,把内容加载到上下文

• 按 SKILL.md 里的指令执行,调用对应的 MCP 工具或 API

这个设计挺聪明的地方在于:Skill 是懒加载的。不是把所有 Skill 的内容都塞进 System Prompt,那会把 token 用光。而是只在 System Prompt 里放摘要,需要时才完整加载。

Android 开发者会觉得这很眼熟:这就是 动态 Feature Module 的思路。基础 App 只安装核心功能,用户点击某个功能时才按需下载对应模块。Skill 就是 AI Agent 的 Dynamic Feature——按需激活,不用的时候不占资源。

⚠️ Skill 的局限:SKILL.md 写得好不好,完全决定 Agent 能不能正确执行。写得含糊,Agent 会乱来;写得太死,又失去了 LLM 的灵活性。这是个需要反复打磨的工程,不是写一次就完的文档。

七、Safety Layer:如何防止 Agent 乱来

这是所有 Agent 框架都绕不开的问题,OpenClaw 的处理方式值得单独说。

四层安全防线

1. Prompt 层:System Prompt 里写清楚哪些操作需要用户确认(删除、支付、发送消息到外部等),哪些可以直接做。这是第一道防线,也是最容易被绕过的一道——精心构造的 prompt injection 可以欺骗模型忽略它。

2. 工具层:敏感工具(比如 exec 执行 elevated 命令)可以设置需要用户 /approve 才能运行。工具调用本身是代码,不受 LLM 的 prompt 影响,这层是可信的。

3. 网络访问控制层:OpenClaw 内网版对出网请求有 allowlist,不在名单里的 IP/域名访问会被拦截,并引导用户申请权限。这层防止 prompt injection 触发的数据外泄。

4. 沙箱层:exec 默认在容器内运行,elevated 模式才能访问宿主机。这是最硬的一层,即使前三层全部失效,最坏的情况也只是容器内的破坏。

这个分层设计和 Android 的权限系统很像:

• Manifest 声明权限 ≈ Prompt 层(告知意图)

• 运行时弹窗授权 ≈ /approve 机制(敏感操作确认)

• SELinux 策略 ≈ 网络访问控制(系统级强制)

• 应用沙箱隔离 ≈ exec 容器沙箱(最后防线)

Andrej Karpathy 最近说,他觉得 AI Agent 最大的能力边界不是技术,而是信任——你愿意让它做多少事,取决于你对它的信任程度。而信任是靠安全机制积累的,不是靠一个聪明的 System Prompt。

八、把这些机制组合起来看

现在我们可以用一个完整的时序来描述:你发了一条消息"帮我看看今天的代码提交有没有问题",到最终收到回复,中间发生了什么。

代码语言:javascript
复制
用户 → 企业微信
企业微信 → OpenClaw Gateway
[路由层] 根据发送者 ID 找到 Session
[组装层] 把消息 + 历史 + System Prompt
+ 工具列表组装成 LLM 请求
[执行层] 调用 LLM APILLM → 返回 tool_use: exec(git log ...)
[工具执行] 运行 git 命令
[结果回填] 把输出追加进消息历史LLM → 返回 tool_use: exec(git diff ...)
[工具执行] 运行 diff 命令
[结果回填] 追加历史LLM → 返回文本(分析结论)
[无更多工具调用,退出循环]Gateway → 发回企业微信
用户 ← 收到回复

整个过程对用户透明,看起来就是"发了条消息,收到了回复"。但中间可能跑了 3-5 轮 LLM 请求,执行了多次工具调用。

如果换成更复杂的任务(比如写一篇文章),主 Agent 会直接 spawn 一个子 Agent,整个执行过程异步进行,完成时才通知你。你不用等在那里盯着。

最后说几句

拆完这些机制,有个感受:Agent 框架本质上是在用工程手段弥补 LLM 的先天不足

LLM 没有持久记忆 → 靠文件系统和每轮注入解决。

LLM 无法主动执行 → 靠工具调用和执行循环解决。

LLM 单次处理有限 → 靠上下文压缩和子 Agent 解决。

LLM 不可信 → 靠分层安全机制解决。

Andrej Karpathy 说他的个人编程瓶颈已经从"能不能写代码"变成了"token 吞吐量"——他调度多个 Agent 并行工作,自己主要做决策和验收。这个状态其实和 OpenClaw 的设计目标是一致的:AI 执行,人来把关

对 Android 开发者来说,这套机制里的很多概念并不陌生。不同的是,以前我们是在写让人用的软件,现在我们可能还需要理解怎么写让 AI Agent 用的系统——接口怎么设计、工具描述怎么写、安全边界怎么划定。

这个问题值得展开说说。

九、写给 Agent 用的系统,和写给人用的有什么不同

接口设计:从"人能看懂"到"模型能推断"

给人用的 API,设计原则是"直觉友好"——命名符合行业惯例、错误码有文档、参数顺序合理。

给 Agent 用的工具,还有一个额外要求:模型必须能从 description 里推断出什么时候该用这个工具,以及怎么用。这两件事对人来说是常识,对模型来说是 Prompt。

举个具体的例子。同样是"读文件"这个操作,对人来说只要知道函数名叫 readFile 就够了。但对 Agent,工具描述里需要说清楚:

• 支持哪些文件类型(文本、图片?)

• 大文件怎么处理(有没有分页,怎么分页)

• 什么情况下用这个而不是用 exec + cat

最后这条很关键。当工具箱里有两个功能重叠的工具,模型会困惑——或者更糟,它会随机选一个。描述里要写清楚各自的适用场景,帮模型做决策。这是人机接口设计里几乎不存在的问题,但在 Agent 工具设计里是第一优先级。

Android 里有个类似的问题:SharedPreferencesDataStore 功能高度重叠,官方文档花了大量篇幅解释为什么推荐后者、什么时候还能用前者。开发者读了文档才知道该选哪个。模型没法主动去读文档,所以这段"选择逻辑"得内嵌在工具描述里。

工具粒度:太细和太粗都是问题

工具粒度是个反直觉的设计难点。

太细:模型需要调用很多次工具才能完成一件事,每次调用都消耗一轮 LLM 推理,成本高、延迟长,中间还容易出错。假设你把"查数据库"拆成了"建连接"+"执行 SQL"+"关连接"三个独立工具,模型就得调三次,中间有很多可以出错的地方。

太粗:工具变成黑盒,参数变成一段自然语言描述,模型不知道边界在哪,容易产生幻觉式调用——它以为这个工具能做到某件事,但其实不行,错误还不明显。

比较合理的原则是:一个工具对应一个完整的原子操作,有清晰的输入输出边界,不需要调用方关心内部实现细节。这和写好的函数是同一个标准——单一职责,但职责要完整。

还有一个容易忽略的点:工具的返回值也要为模型优化。返回一段 JSON 里嵌套了七八层,和返回一段扁平的、关键信息放在最前面的结构,对模型理解的难度差别很大。模型不像人眼那样能快速扫描层级结构,它是顺序 token 处理的,关键信息越早出现越好。

错误处理:要让模型能从错误里恢复

人用的 API,抛一个异常带 stack trace,开发者能看懂、能 debug。

Agent 用的工具,返回错误时需要额外考虑:模型看到这个错误,能不能判断出下一步该怎么办

一个反面例子:

代码语言:javascript
复制
{
"error": "ENOENT: no such file or directory"
}

一个对模型友好的版本:

代码语言:javascript
复制
{
"error": "File not found: /root/config.json",
"suggestion": "Use the 'read' tool with a
different path, or use 'exec' with
'find' to locate the file first."
}

后者多了一个 suggestion 字段,直接告诉模型下一步的选择。这不是过度设计,而是在帮模型减少不确定性——越少的不确定性,模型就越不容易乱走。

本质上,这是把原来写在注释或文档里的"常见错误处理建议",移到了运行时的错误返回值里。因为模型没法在出错时去查文档。

安全边界:不要靠模型自觉

这是最容易踩的坑:把安全约束写在 System Prompt 里,然后以为就完事了

Prompt 里的约束对正常请求有效,但有两种情况会失效:一是用户(或第三方内容)里有 prompt injection,诱导模型绕过约束;二是模型在长上下文里"忘掉"了早期的约束。

正确的做法是:安全约束必须在工具层或系统层强制执行,Prompt 只是第一道提示,不是最后一道防线

具体操作上,可以参考几个原则:

最小权限:工具默认不授予超出任务需要的权限。写文件的工具不应该同时能删文件;读数据库的工具不应该同时能执行 DDL。这和 Android 的权限粒度思路完全一致——READ_CONTACTSWRITE_CONTACTS 是分开的,不是一个。

高风险操作需要运行时确认:不可逆的操作(删除、外发消息、支付)在工具执行层加一道门,强制要求人工确认,不论模型是否在 Prompt 里被告知"可以做"。

外部输入全部不可信:网页内容、邮件内容、数据库里的字符串——任何来自系统外部的内容,都可能含有 prompt injection。工具在返回这些内容时,可以加一个明确的标记(OpenClaw 的做法是加 EXTERNAL_UNTRUSTED_CONTENT 包裹),帮模型区分"系统指令"和"外部数据"。

一个很有启发的思维模型:把 AI Agent 当成一个刚入职的实习生。他聪明、听话、执行力强,但你不会给他直接操作生产数据库的权限,不会让他独立决定是否向外发重要邮件。不是因为他不靠谱,而是因为好的系统设计本来就不应该依赖个体的自觉性。

一个新的职业技能点正在形成

回到开头的问题:写给 AI Agent 用的系统,和写给人用的系统,区别到底在哪?

如果要我给一个简短的回答:给人用的系统,用户能看文档、能理解报错、能凭直觉探索;给 Agent 用的系统,这些都要内嵌在接口和返回值本身里

这对 Android 开发者来说其实是一个很有意思的视角切换。我们已经很熟悉"面向开发者设计 API"、"面向用户设计交互",现在多了一个层次:面向 LLM 设计工具

这个层次有自己的设计原则,有自己的常见反模式,也有自己的评测方法(Anthropic 那份关于 Agent Evals 的文章已经把这件事系统化了)。

这不是未来的技能——现在就在发生。随着越来越多的系统开始暴露工具给 AI Agent 调用,懂得怎么设计"Agent-friendly API"的工程师,会比只懂怎么设计"Human-friendly API"的工程师多一块很重要的能力拼图。

Andrej Karpathy 说未来工程师的瓶颈是"token 吞吐量"。但在那之前,还有一个更基础的瓶颈:有没有足够好用的工具可以给 Agent 调。这个工具,得工程师来建。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档