Skip to content

状态与上下文管理

为什么需要状态管理

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用户输入
modelLLM 响应(文本或工具调用)
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 的压缩逻辑:

  1. 监控 token 使用量
  2. 达到阈值时触发压缩
  3. 保留最近的消息,压缩早期消息
  4. 发射 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 陷入无限循环:循环检测 →

通过实际源码学习 AI Agent 开发