runtime-ask

可以,我们把 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 的关键。

相关推荐
Rust研习社1 小时前
90% 的 Rust 新手都不知道的 3 个实用开发技巧
后端·rust·编程语言
ZengLiangYi1 小时前
sql.js WASM 深度解析
javascript·数据库·后端
Stick_ZYZ1 小时前
从“能调用工具”到“能稳定执行任务”:Agent 工程化的下一步
java·人工智能·后端·spring·ai
千云2 小时前
使用Dubbo延迟暴露解决启动接口超时,开发人员再也不用熬夜了!
后端
JustHappy2 小时前
古法编程秘籍(三):为什么需要函数?因为程序员讨厌重复劳动
前端·javascript·后端
用户2181697049302 小时前
Gin (六) mysql的操作 gin操作mysql
后端
AI打工人2 小时前
Python并发编程:多线程与多进程实战指南
后端
Jiude2 小时前
AI面对真机调试也束手无策?我将方法论形成了一套SKILL 🛠️🤖
前端·后端·测试
千云2 小时前
AI Coding 落地探索日志·实践篇·提效操作指南
后端