状态与上下文管理
为什么需要状态管理
Agent 需要记住:
- 对话历史(用户说了什么,AI 做了什么)
- 工具执行结果
- 当前任务进度
没有状态,每轮对话都是全新的,Agent 无法完成多步骤任务。
消息历史
基本结构
typescript
interface Message {
role: 'user' | 'model' | 'function'
content: string
name?: string // function 消息需要
}
const messages: Message[] = [
{ role: 'user', content: '帮我创建一个 hello.ts 文件' },
{ role: 'model', content: '', functionCall: { name: 'write_file', args: {...} } },
{ role: 'function', name: 'write_file', content: '{"success": true}' },
{ role: 'model', content: '已创建 hello.ts 文件' },
]消息角色
| 角色 | 说明 |
|---|---|
user | 用户输入 |
model | LLM 响应(文本或工具调用) |
function | 工具执行结果 |
gemini-cli 的 Turn 系统
gemini-cli 使用 Turn(轮次)来组织对话。
源码位置:packages/core/src/core/turn.ts
typescript
// Turn 事件类型
type GeminiEventType =
| 'Content' // 文本内容
| 'ToolCallRequest' // 工具调用请求
| 'ToolCallResponse' // 工具执行结果
| 'Thought' // 思考过程
| 'Error' // 错误
| 'ChatCompressed' // 对话被压缩
| 'LoopDetected' // 检测到循环
// Turn 结构
interface Turn {
events: GeminiEvent[]
timestamp: number
}这种设计的好处:
- 每个事件有明确类型
- 方便 UI 渲染不同类型的内容
- 便于调试和日志记录
Token 限制
问题
LLM 有上下文窗口限制(如 128K tokens)。对话越长,占用 token 越多。
解决方案:对话压缩
当 token 接近限制时,压缩早期对话:
typescript
async function compressConversation(messages: Message[]) {
// 计算当前 token 数
const tokenCount = countTokens(messages)
if (tokenCount > TOKEN_LIMIT * 0.8) {
// 取早期消息
const earlyMessages = messages.slice(0, messages.length / 2)
// 让 LLM 总结
const summary = await llm.summarize(earlyMessages)
// 替换为摘要
return [
{ role: 'system', content: `之前的对话摘要:${summary}` },
...messages.slice(messages.length / 2)
]
}
return messages
}gemini-cli 的压缩策略
源码位置:packages/core/src/services/chatCompression.ts
gemini-cli 的压缩逻辑:
- 监控 token 使用量
- 达到阈值时触发压缩
- 保留最近的消息,压缩早期消息
- 发射
ChatCompressed事件通知 UI
typescript
class ChatCompressionService {
async maybeCompress(messages: Message[], tokenLimit: number) {
const currentTokens = await this.countTokens(messages)
if (currentTokens < tokenLimit * 0.75) {
return messages // 不需要压缩
}
// 找到压缩点
const splitPoint = this.findSplitPoint(messages)
// 总结早期内容
const summary = await this.summarize(messages.slice(0, splitPoint))
// 构建新的消息列表
return [
{ role: 'system', content: `对话历史摘要:\n${summary}` },
...messages.slice(splitPoint)
]
}
}状态持久化
会话恢复
gemini-cli 支持保存和恢复会话状态。
typescript
// 保存状态
function saveCheckpoint() {
const state = {
messages: conversation.messages,
workingDirectory: process.cwd(),
timestamp: Date.now()
}
fs.writeFileSync('checkpoint.json', JSON.stringify(state))
}
// 恢复状态
function loadCheckpoint() {
const state = JSON.parse(fs.readFileSync('checkpoint.json', 'utf-8'))
conversation.messages = state.messages
process.chdir(state.workingDirectory)
}gemini-cli 的 ChatRecording
源码位置:packages/core/src/services/chatRecording.ts
gemini-cli 自动保存会话记录,支持:
- 自动保存(每 N 轮)
- 手动保存(/checkpoint 命令)
- 崩溃恢复
内存管理最佳实践
1. 只保留必要的历史
typescript
// 工具输出可能很大,适当截断
function truncateToolResult(result: string, maxLength = 5000) {
if (result.length <= maxLength) return result
return result.slice(0, maxLength) + '\n...(输出已截断)'
}2. 定期清理
typescript
// 删除过旧的消息
function pruneOldMessages(messages: Message[], keepLast = 50) {
if (messages.length <= keepLast) return messages
return messages.slice(-keepLast)
}3. 摘要而非原文
对于大型工具输出,存储摘要:
typescript
// 而不是存储整个文件内容
messages.push({
role: 'function',
name: 'read_file',
content: '文件内容: config.json (2KB, JSON 格式, 包含数据库配置)'
})完整状态管理示例
typescript
class ConversationManager {
private messages: Message[] = []
private tokenLimit = 100000
async addUserMessage(content: string) {
this.messages.push({ role: 'user', content })
}
async addModelResponse(response: LLMResponse) {
if (response.text) {
this.messages.push({ role: 'model', content: response.text })
}
}
async addToolResult(name: string, result: unknown) {
const content = JSON.stringify(result)
this.messages.push({
role: 'function',
name,
content: truncateToolResult(content)
})
// 检查是否需要压缩
await this.maybeCompress()
}
async maybeCompress() {
const tokens = await countTokens(this.messages)
if (tokens > this.tokenLimit * 0.8) {
this.messages = await compressConversation(this.messages)
}
}
getMessages() {
return [...this.messages]
}
save(path: string) {
fs.writeFileSync(path, JSON.stringify(this.messages))
}
load(path: string) {
this.messages = JSON.parse(fs.readFileSync(path, 'utf-8'))
}
}小结
- 消息历史是 Agent 的记忆
- 使用 Turn 系统组织不同类型的事件
- Token 限制需要对话压缩
- 支持状态持久化便于会话恢复
下一步
状态管理讲完了,接下来学习如何防止 Agent 陷入无限循环:循环检测 →