状态、工具与执行轨迹
状态、工具与执行轨迹 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完本课你能给多步 AI 任务装上「黑匣子」:任务状态落库、每步工具的输入输出有轨迹可查,失败后从断点继续而不是把做对的部分重做一遍。
问题场景
一个五步任务(读需求 → 检索资料 → 起草 → 校验 → 保存)跑到第四步模型调用超时。最糟的重来方式:整个任务从头跑——前三步重新检索重新起草,用户盯着界面看着所有步骤的进度条倒回去,已花的调用费再花一遍。用户不知道任务是死是活,你也说不清错在哪步。缺的是两样东西:显式的状态(每步完成没有)和执行轨迹(每步发生了什么)。
核心解释
先看本站已有的雏形:聊天场景里,ai_conversation(会话)、ai_message(消息,status 有 streaming/done/error)、ai_call(每次模型调用的输入输出)三层分离——消息是业务状态,调用是执行轨迹。A13 的占位消息、A12 的断流保存,本质都是「状态显式化」。多步任务把同一思路推广成两张表:
-- 任务:整体状态 + 当前进行到哪一步
CREATE TABLE task (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL, -- 任务类型(如 import / assist_review)
status TEXT NOT NULL, -- pending / running / failed / done / cancelled
current_step TEXT, -- 当前步骤 key,断点恢复的定位依据
created_at TEXT NOT NULL
);
-- 步骤:每步的输入输出与轨迹(跑过就有记录,复跑不覆盖,追加新记录)
CREATE TABLE task_step (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
step_key TEXT NOT NULL, -- 如 "retrieve" / "draft" / "verify"
status TEXT NOT NULL, -- done / failed
tool_name TEXT, -- 用了什么工具(模型调用时记录模型名)
input_digest TEXT, -- 入参摘要(截断存储)
output_digest TEXT, -- 结果摘要(截断存储,同工具结果 8000 上限的思路)
created_at TEXT NOT NULL
);
恢复逻辑就一行判断:重启时查 current_step,从第一个非 done 的步骤继续,done 的步骤直接读 task_step 里已存的输出。
执行轨迹要记什么?对照工具循环的每一步:
// 工具执行处加轨迹记录(A15 的 executeTool 外围)
const started = Date.now();
const exec = executeTool(name, args);
await recordStep(taskId, {
stepKey: `tool_${name}_${i}`,
status: exec.result.includes("error") ? "failed" : "done",
toolName: name,
inputDigest: args.slice(0, 200),
outputDigest: exec.result.slice(0, 500), // 摘要进轨迹,全量在工具结果里回传模型
});
如何验证:人为在第 3 步制造失败(如把模型配置改错),重启任务,确认:前两步没有重新执行(看 task_step 时间戳没有新增)、第三步从失败点重跑、界面(或日志)能回答「错在第几步、那步的输入是什么」。
示例拆解
一次失败恢复的完整数据视图:
{
"task": { "id": "task_01", "kind": "assist_review", "status": "running", "current_step": "draft" },
"steps": [
{ "step_key": "read_requirement", "status": "done", "tool_name": "get_lesson", "output_digest": "读到需求:为导入加校验…" },
{ "step_key": "search_material", "status": "done", "tool_name": "search_documents", "output_digest": "命中 3 个片段…" },
{ "step_key": "draft", "status": "failed", "tool_name": "chat", "output_digest": "上游超时(unreachable)" }
]
}
关键点:
- 状态和轨迹是两种读法:
task.status回答「现在进行到哪」,task_step回答「每步发生了什么」。排查问题看轨迹,恢复执行看状态。 - 步骤结果要可复用:done 步骤的输出摘要(甚至全量)存下来,恢复时直接喂给下一步,这就是「不从头重复」的底气。注意与 A27 的幂等配合:可复用的前提是「重做也不会产生副作用」。
- 轨迹摘要与全量分开:轨迹里存摘要(供人看、供统计),工具结果的完整内容按调用上限截断后走正常链路(A15)。全塞轨迹里,表比上下文还先爆。
- 界面展示即轨迹:A10 协议里的
tool事件(running/done/error)就是轨迹的实时推流;落库的 task_step 是它的持久版本。用户看到的步骤进度,和事后复盘看到的是同一份数据。
动手任务
- 在纸上(或实验代码里)为「资料问答任务」画出 task/task_step 两张表的各一行数据:正常完成时五个步骤各长什么样。
- 推演失败恢复:第三步失败后重启,写出恢复函数的伪代码——先查什么、跳过什么、从哪继续。
- 对照站内助手日志:触发一次工具调用,找出它在消息表(ai_message 的 status/refs)和界面事件里分别留下了什么痕迹——验证「轨迹有实时与持久两个版本」。
常见错误
- 状态只在内存里:进程一挂状态全丢,恢复无从谈起。多步任务的状态必须落库。
- 复跑覆盖旧记录:task_step 用 upsert,失败原因被第二次重跑的记录抹掉,复盘时查无对证。追加,不覆盖。
- 恢复后把 done 步骤重跑一遍:状态存了但不读,等于白存。恢复入口永远先查 current_step。
- 轨迹只记成功:failed 步骤的 input_digest 是定位问题的唯一线索(那一步喂了什么导致失败),失败更要记全。
检查与复盘
自查清单:
- 能说出任务表与步骤表各自回答什么问题
- 能写出断点恢复的判定逻辑(查 current_step、跳过 done、从失败步继续)
- 能对照本站聊天实现说出「消息状态」与「调用记录」分别对应哪张表
参考资料
- OpenAI 文档:Function calling —— 工具循环各环节(轨迹的来源)
- SQLite 官方文档:CREATE TABLE —— 任务/步骤表的设计基础
- 下一课 A27 讲恢复的另一半保障:重试时如何保证不重复写入。
读完这一课了?
标记完成后,工作台会安排接下来的学习。