可以,我们把 ask() 当成一个回合制游戏来看。
用户说一句话以后,agent 不是马上回答,而是一轮一轮地做事:
看上下文 → 问模型 → 模型决定下一步 → 执行工具或返回答案 → 记录结果 → 下一轮
源码里 ask() 的注释也明确说了:它是 runtime 的总调度器,负责记录会话、组 prompt、调用模型、执行工具、写 trace/report、更新状态,直到模型给出最终答案或系统停止。
1. 先看最简单版流程图
你先别管 checkpoint、trace、metadata,那些都是"增强功能"。
核心流程其实是这样:
scss
用户输入 user_message
↓
ask() 开始
↓
记录用户说了什么
↓
创建一个 TaskState
↓
进入循环
↓
构造 prompt
↓
调用模型
↓
parse 模型输出
↓
模型想调用工具?
┌────┴────┐
是 否
↓ ↓
run_tool final answer?
↓ ↓
记录工具结果 返回最终答案
↓
回到循环
更像人话:
markdown
用户:帮我修 bug
ask:
好,我先记下来。
然后我创建一个任务记录。
接下来我反复问模型:
你下一步想干嘛?
模型可能说:
我要读文件。
我要改文件。
我要跑测试。
我已经完成了。
如果模型要用工具:
ask 就调用 run_tool 去执行。
执行完,把结果记下来。
再问模型下一步。
如果模型说完成了:
ask 就返回最终答案。
2. ask() 可以分成三大段
你读源码时,不要从第一行读到最后一行。
你只看这三段:
第一段:开场准备
第二段:主循环
第三段:收尾返回
第一段:开场准备
源码大概做这些事:
lua
run_started_at = time.monotonic()
self.memory.set_task_summary(user_message)
self.record({
"role": "user",
"content": user_message,
"created_at": now()
})
task_state = TaskState.create(...)
self.current_task_state = task_state
self.current_run_dir = self.run_store.start_run(task_state)
self.emit_trace(task_state, "run_started", ...)
你不用记每一行。
你只需要知道:
markdown
ask() 一开始做 4 件事:
1. 记住用户这次要干嘛
2. 把用户消息写进 history
3. 创建 TaskState,表示"这是一项新任务"
4. 写一条 trace:任务开始了
你可以把它理解成:
arduino
老师给学生发了一张任务单:
任务:帮我修 bug
任务 ID:task_xxx
运行 ID:run_xxx
当前状态:running
开始时间:now
这里的 TaskState 就是任务进度表。
第二段:主循环
ask() 最重要的是这个循环:
lua
while tool_steps < self.max_steps and attempts < max_attempts:
翻译成人话:
只要还没超过最大工具步数,
也还没超过最大尝试次数,
agent 就可以继续思考和行动。
里面每一轮都做同一套动作:
markdown
1. attempts + 1
2. 构造 prompt
3. 调模型
4. parse 模型输出
5. 根据 parse 结果分支处理
你可以把这一轮想成:
一回合开始
↓
把当前情况整理给模型
↓
问模型:你下一步要干嘛?
↓
模型回答
↓
runtime 判断模型回答属于哪一类
3. 主循环伪代码
这是 ask() 的小学生版伪代码:
ini
def ask(user_message):
# ---------- 开场 ----------
记住用户这次的任务
把用户消息写入 history
创建 TaskState
创建 run 目录
写 trace:run_started
tool_steps = 0
attempts = 0
# ---------- 主循环 ----------
while 没超过最大工具步数 and 没超过最大尝试次数:
attempts += 1
# 1. 构造 prompt
prompt = 把系统规则、工具列表、历史、记忆、workspace 状态拼起来
写 trace:prompt_built
# 2. 调模型
raw = model_client.complete(prompt)
# 3. 解析模型输出
kind, payload = parse(raw)
写 trace:model_parsed
# 4. 如果模型要调用工具
if kind == "tool":
tool_steps += 1
name = payload["name"]
args = payload["args"]
result = run_tool(name, args)
把工具结果写入 history
写 trace:tool_executed
创建 checkpoint
continue # 回到下一轮
# 5. 如果模型输出格式错了
if kind == "retry":
把 retry 提示写入 history
continue # 让模型下一轮重新输出
# 6. 如果模型给了最终答案
if kind == "final":
final = payload
把 final 写入 history
标记 task_state 成功
保存 durable memory
创建 checkpoint
写 trace:run_finished
写 report
return final
# ---------- 如果循环被迫停止 ----------
if 尝试次数太多:
return "模型格式错误太多,停止"
else:
return "达到最大工具步数,停止"
你现在只要看懂这段伪代码,ask() 就已经懂了一半以上。
4. 最关键的一句话
ask() 不是"回答问题"的函数。
它是:
管理 agent 一轮又一轮行动的函数。
它每一轮都在问:
模型,你下一步要干嘛?
模型只能回答三种东西:
markdown
1. tool:我要用工具
2. final:我完成了
3. retry:我输出格式错了,请重来
所以整个 ask() 其实就是:
python
while True:
问模型下一步
if 模型要用工具:
安全执行工具
记录结果
继续
elif 模型给最终答案:
保存结果
返回
else:
让模型重试
5. 三种 kind 是理解 ask() 的钥匙
parse(raw) 会返回:
ini
kind, payload = self.parse(raw)
你重点看 kind。
情况一:kind == "tool"
说明模型说:
我现在还不能最终回答,我需要先做一个动作。
比如模型输出:
bash
<tool>{"name":"read_file","args":{"path":"main.py"}}</tool>
parse() 会把它变成:
makefile
kind = "tool"
payload = {
"name": "read_file",
"args": {"path": "main.py"}
}
然后 ask() 做:
ini
result = self.run_tool(name, args)
这一步才是真的读文件、写文件、跑 shell。
执行完以后,ask() 把工具结果写进 history:
csharp
self.record({
"role": "tool",
"name": name,
"args": args,
"content": result,
})
为什么要写进 history?
因为下一轮模型需要知道:
刚才读文件读到了什么?
刚才测试失败在哪里?
刚才 patch 成功了吗?
所以工具结果必须回到上下文里。
情况二:kind == "retry"
说明模型格式写错了。
比如模型应该输出:
bash
<tool>{"name":"read_file","args":{"path":"main.py"}}</tool>
但它输出了坏 JSON:
ruby
<tool>{"name":"read_file", args: ???}</tool>
这时候不能执行工具。
ask() 会把一个提醒写进 history:
xml
Runtime notice: 你输出格式错了。请输出合法的 <tool> 或 <final>。
然后进入下一轮,让模型重新回答。
所以 retry 的作用是:
模型说错话了,不崩溃,不执行危险动作,而是让它修正。
情况三:kind == "final"
说明模型说:
我做完了,可以回答用户了。
比如:
arduino
<final>我已经修复了 bug,并通过了测试。</final>
这时候 ask() 会:
markdown
1. 把 final answer 写入 history
2. 标记 TaskState 成功
3. 保存 durable memory
4. 创建 checkpoint
5. 写 trace
6. 写 report
7. return final
这就是一次 agent run 的结束。
6. 你可以把 ask() 画成这个超简图
kotlin
ask(user_message)
│
├─ 1. 记录用户输入
│
├─ 2. 创建任务状态 TaskState
│
├─ 3. while 还能继续:
│ │
│ ├─ build prompt
│ │
│ ├─ model_client.complete(prompt)
│ │
│ ├─ parse(raw)
│ │
│ ├─ tool?
│ │ ├─ run_tool()
│ │ ├─ 记录工具结果
│ │ ├─ 创建 checkpoint
│ │ └─ continue
│ │
│ ├─ retry?
│ │ ├─ 记录 retry notice
│ │ └─ continue
│ │
│ └─ final?
│ ├─ 记录最终答案
│ ├─ 标记成功
│ ├─ 写 checkpoint
│ ├─ 写 report
│ └─ return final
│
└─ 4. 超过限制就停止
7. 真实变量翻译表
看源码时你会看到这些变量。不要怕,它们其实都很直白。
| 变量 / 方法 | 你可以理解成 |
|---|---|
user_message |
用户这次说的话 |
self.record(...) |
写入聊天历史 |
TaskState |
这次任务的进度表 |
run_store |
保存这次运行过程的地方 |
emit_trace(...) |
记日志:刚刚发生了什么 |
tool_steps |
已经用了几次工具 |
attempts |
已经问了模型几次 |
max_steps |
最多允许用几次工具 |
max_attempts |
最多允许模型乱输出/重试多少次 |
_build_prompt_and_metadata(...) |
给模型准备上下文 |
model_client.complete(...) |
调用大模型 |
raw |
模型原始输出 |
parse(raw) |
判断模型输出是 tool / final / retry |
run_tool(name, args) |
安全执行工具 |
checkpoint |
当前任务进度快照 |
report |
这次运行的最终报告 |
8. 面试里怎么讲 ask()
你可以先背这个版本:
ask()是 agent runtime 的主控制循环。用户输入后,它先记录 user message,并创建一个TaskState来跟踪这次运行。然后进入一个受限循环,每一轮都会构造 prompt、调用模型、解析模型输出。模型输出只能被解释成三类:tool call、final answer 或 retry。如果是 tool call,runtime 会通过run_tool()做校验、审批和安全执行,然后把结果写回 history、trace、memory 和 checkpoint,再进入下一轮。如果是 final answer,就保存结果、写 report 并返回。如果模型持续输出错误格式,或者工具步数超过限制,runtime 会主动停止,避免死循环。
这段已经是一个合格的面试回答。
9. 你现在读源码时只盯这些行
打开 ask(),只找这些关键词:
csharp
self.record({"role": "user", ...})
意思:记录用户输入。
lua
TaskState.create(...)
意思:创建任务状态。
lua
while tool_steps < self.max_steps and attempts < max_attempts:
意思:进入 agent 主循环,但有上限。
ini
prompt, prompt_metadata = self._build_prompt_and_metadata(user_message)
意思:准备给模型看的上下文。
ini
raw = self.model_client.complete(...)
意思:问模型下一步。
ini
kind, payload = self.parse(raw)
意思:把模型输出分类。
ini
if kind == "tool":
意思:模型要用工具。
ini
result = self.run_tool(name, args)
意思:安全执行工具。
ini
if kind == "retry":
意思:模型格式错了,让它重来。
kotlin
task_state.finish_success(final)
return final
意思:模型给最终答案,任务结束。
10. 你现在只需要记住这个版本
markdown
ask() 就是一个循环:
记录用户问题
创建任务
while 还能继续:
组 prompt
问模型
parse 模型输出
如果模型要用工具:
run_tool()
记录工具结果
继续下一轮
如果模型格式错:
提醒模型重试
继续下一轮
如果模型给最终答案:
保存结果
返回答案
如果循环太久:
主动停止
下一步我们拆 parse():它比 ask() 小很多,而且是理解 tool call 的关键。