会话与消息模型
Agent 的会话日志应该记录“执行轨迹”,不只是聊天文本。
一个最小消息模型可以是:
ts
type Message =
| { type: "user"; id: string; content: string }
| { type: "assistant"; id: string; content: AssistantBlock[] }
| { type: "tool_result"; id: string; toolUseId: string; content: string; isError?: boolean }
| { type: "system"; id: string; content: string }1
2
3
4
5
2
3
4
5
Claude Code 的消息类型更多,但核心仍然围绕:
- user。
- assistant。
- tool use。
- tool result。
- system。
- attachment。
- compact boundary。
- progress。
tool_use 和 tool_result 配对
这是消息模型里最重要的不变量。
text
assistant message:
tool_use id=abc name=Read
user message:
tool_result tool_use_id=abc content=...1
2
3
4
5
2
3
4
5
如果 tool_result 缺失,下一轮模型调用可能失败。因此 Claude Code 有逻辑修复缺失或孤立的工具结果。
自研 Agent 第一版不一定要自动修复,但应该在写入消息时保证配对正确。
源码里相关锚点包括:
normalizeMessagesForAPI():把内部消息转换成 API 可接受格式。ensureToolResultPairing():处理 tool use/result 配对不完整的问题。createToolResultStopMessage():在异常、中断或停止场景中补出错误型工具结果。
会话日志
Claude Code 使用 append-only JSONL 记录 transcript。这个设计值得学习。
JSONL 的优点:
- 每条消息独立追加。
- 程序崩溃时已有内容不容易丢。
- 可以流式读取。
- 方便恢复。
- 方便做 session list。
最小实现:
ts
export class SessionStore {
async append(message: Message) {
await fs.appendFile(this.path, JSON.stringify(message) + "\n")
}
async load() {
const lines = await fs.readFile(this.path, "utf8")
return lines.trim().split("\n").map(line => JSON.parse(line))
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
小结
会话日志是 Agent 的黑匣子。只存最终回答是不够的,必须保存工具调用、工具结果、错误和关键元数据。