📖 第一部分:事故背景

一、 赛博大案:一个低级失误引发的“百亿级代码裸奔”

2026年3月31日,对于大模型巨头 Anthropic 来说,绝对是灾难性的一天。他们重磅推出的终端 AI 编程神器 —— Claude Code (CC),其核心源代码在毫无防备的情况下,被全网“白嫖”了。

更有戏剧性的是,这并非什么高超的黑客渗透,而是一个极其低级的“软件供应链前端工程化失误”

1. 案发现场:成也 npm,败也 Source Map
Anthropic 工程师在发布 Claude Code 的 npm 包(版本 2.1.88)时,虽然对核心 TypeScript 代码进行了编译和混淆(变成了难以阅读的 cli.js),但却致命地忘记了剔除 .map 文件(Source Map 映射文件)!

更可怕的是,这个高达 57MB 的 cli.js.map 文件中,由于打包配置的失误,其 sourcesContent 字段竟然完整地内嵌了所有未混淆的原始代码字符串。

2. 极客狂欢与法务追杀
只需要区区十来行 Node.js 脚本遍历这个 JSON,近 2000 个原汁原味的 TypeScript 官方源文件(包含核心工具定义、拦截器 Hooks、乃至绝密的 System Prompt)就在本地硬盘上被无损还原了。
尽管 Anthropic 官方在几小时后火速祭出了 DMCA(数字千年版权法)大棒,逼迫 GitHub 物理锁定了所有传播仓库的 Issues 与源码,但代码的快照早已在极客圈传开。

3. 吃瓜之外的思考
作为一名 AI 后端开发者,我连夜拉取了这份“绝版”快照。剥开它神秘的外衣,我惊叹地发现:原来全球最顶级的 Agent 系统,其核心的稳定性并非依赖大模型自身的“涌现与智商”,而是靠着一套极其森严、严丝合缝的传统工程代码在“兜底”。

接下来,我将结合这份价值 25 亿刀的源码,为你深度拆解:顶级大厂是如何通过代码解决 AI Agent 落地时的那些致命痛点的。


💡 第二部分:正文(痛点+解决方案+大白话解释+源码)

1. 上下文窗口管理

痛点

AI 模型有 token 上限(比如 20 万字),不同模型上限还不一样。聊多了就会"爆内存",直接报错。而且用户可能用不同的模型(Claude 3.5/4/Opus),怎么动态适配?

解决方案

分层配置 + 动态计算 + 环境变量覆盖

  • 默认 200K,支持 1M(通过标记或 beta 头)
  • 环境变量可以强制覆盖
  • 自动计算有效窗口(减去输出 token 预算)

大白话解释

想象你租了不同大小的仓库(模型):

  • 有的仓库 200 平米,有的 1000 平米
  • 你要动态知道当前仓库多大
  • 还要留出空间放"出货"(AI 回复)
  • 管理员可以通过环境变量临时调整大小
// context.ts
// 默认 200K 上下文窗口
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000

// 动态获取模型上下文窗口
export function getContextWindowForModel(model: string, betas?: string[]): number {
  // 1. 环境变量覆盖(最高优先级)
  if (process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS) {
    const override = parseInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, 10)
    if (!isNaN(override) && override > 0) return override
  }
  
  // 2. [1m] 后缀 — 显式启用 1M 上下文
  if (has1mContext(model)) {
    return 1_000_000
  }
  
  // 3. Beta 头启用 1M
  if (betas?.includes(CONTEXT_1M_BETA_HEADER) && modelSupports1M(model)) {
    return 1_000_000
  }
  
  // 4. 默认值
  return MODEL_CONTEXT_WINDOW_DEFAULT
}

// 计算有效窗口(减去输出预算)
export function getEffectiveContextWindowSize(model: string): number {
  const contextWindow = getContextWindowForModel(model)
  const maxOutputTokens = getMaxOutputTokensForModel(model)
  return contextWindow - maxOutputTokens
}

2. 上下文压缩

痛点

对话越来越长,快到上限了怎么办?直接删历史会丢失信息,不删又无法继续。怎么在"省钱"和"不丢信息"之间平衡?

解决方案

自动检测 + 生成摘要 + 保留最近消息 + 恢复关键文件

  • 达到阈值(187K/200K)自动触发
  • 用另一个 AI 写摘要(复用缓存省钱)
  • 保留最近 10 条消息(保证连贯)
  • 重新读取最近用过的 5 个文件(防失忆)

大白话解释

想象你的笔记本快写满了:

  • 系统说:“快满了,我帮你整理一下”
  • 把前 900 页的内容写成 1 页摘要
  • 保留最近 100 页完整内容
  • 把之前贴的重要便签重新贴回去
  • 以后翻笔记先看摘要,需要细节再翻后面
// compact.ts
// 压缩主流程
export async function compactConversation(
  messages: Message[],
  context: ToolUseContext,
  cacheSafeParams: CacheSafeParams,
  isAutoCompact: boolean = false,
): Promise<CompactionResult> {
  // 1. 计算压缩前 token 数
  const preCompactTokenCount = tokenCountWithEstimation(messages)
  
  // 2. 用 Forked Agent 生成摘要(复用主对话缓存)
  const summary = await streamCompactSummary({
    messages: messagesToSummarize,
    summaryRequest: createUserMessage({ content: '请总结以上对话' }),
    cacheSafeParams,  // 关键:复用主 Agent 缓存,省钱!
  })
  
  // 3. 创建压缩边界标记
  const boundaryMarker = createCompactBoundaryMessage(
    isAutoCompact ? 'auto' : 'manual',
    preCompactTokenCount,
    messages.at(-1)?.uuid
  )
  
  // 4. 恢复关键文件(最多 5 个,每个最多 5K tokens)
  const fileAttachments = await createPostCompactFileAttachments(
    preCompactReadFileState,
    context,
    maxFiles: 5,
    maxTokensPerFile: 5000
  )
  
  // 5. 组装压缩后的消息
  return [
    boundaryMarker,      // 标记压缩边界
    createUserMessage({  // 摘要消息
      content: `[历史摘要] ${summary}`,
      isCompactSummary: true,
    }),
    ...fileAttachments,  // 恢复的文件
    ...recentMessages,   // 保留的最近消息
  ]
}

3. Prompt Cache 共享

痛点

压缩需要调用另一个 AI(Forked Agent),如果重新传系统提示+工具定义,会浪费大量 tokens(可能占 80%)。怎么省钱?

解决方案

CacheSafeParams 确保子 Agent 和主 Agent 使用相同参数,命中 Anthropic 的 Prompt Cache

  • 系统提示、工具定义、模型必须完全相同
  • 子 Agent 只传新消息(摘要请求)
  • 其他部分命中缓存,费用降低 90%

大白话解释

想象你请了两个家教:

  • 家教 A 教了你 3 小时(主 Agent)
  • 家教 B 来帮你写总结(子 Agent)

正常情况下:

  • 家教 B 要重新了解你学了什么(贵)

Claude Code 的做法:

  • 家教 B 直接看家教的备课笔记(cache)
  • 只需要写总结(便宜)
  • 省了 90% 的时间和钱
// forkedAgent.ts
// 缓存共享参数定义
export type CacheSafeParams = {
  /** System prompt - must match parent for cache hits */
  systemPrompt: SystemPrompt
  /** User context - prepended to messages, affects cache */
  userContext: { [k: string]: string }
  /** System context - appended to system prompt, affects cache */
  systemContext: { [k: string]: string }
  /** Tool use context containing tools, model, and other options */
  toolUseContext: ToolUseContext
  /** Parent context messages for prompt cache sharing */
  forkContextMessages: Message[]
}

// compact.ts
// 压缩时复用缓存
if (promptCacheSharingEnabled) {
  // 关键注释:不要设置 maxOutputTokens,否则会改变 cache key
  // 子 Agent 通过发送相同的 cache-key 参数来"搭便车"
  
  const result = await runForkedAgent({
    promptMessages: [summaryRequest],  // 只传新消息
    cacheSafeParams,                    // 复用主 Agent 缓存!
    querySource: 'compact',
    forkLabel: 'compact',
    maxTurns: 1,
    skipCacheWrite: true,
  })
}

4. 多 Agent 隔离与协作

痛点

多个 Agent 同时运行时:

  • 怎么隔离上下文?(A Agent 不能访问 B 的数据)
  • 怎么通信?(Leader 怎么给 Teammate 派任务)
  • 怎么选择隔离级别?(同进程 vs 独立进程)

解决方案

AsyncLocalStorage 隔离 + Mailbox 通信 + 多 Backend 支持

  • 同进程:用 AsyncLocalStorage 做上下文隔离
  • 跨进程:用 tmux/iTerm2 做物理隔离
  • 通信:基于文件的 Mailbox 消息队列

大白话解释

想象你是一个项目经理(Leader),有几个员工(Teammates):

同进程(In-Process):

  • 大家都在一个办公室
  • 每个人都有自己的工位(AsyncLocalStorage)
  • 工位之间互相看不到
  • 通过内部信箱(Mailbox)传纸条

跨进程(tmux/iTerm2):

  • 大家在不同的办公室
  • 完全物理隔离
  • 通过外部信箱通信
// spawnInProcess.ts
// 同进程 Agent 的隔离机制
export type InProcessSpawnConfig = {
  name: string           // Agent 名称,如 "researcher"
  teamName: string       // 所属团队
  prompt: string         // 初始任务
  planModeRequired: boolean
  model?: string
}

export type InProcessSpawnOutput = {
  success: boolean
  agentId: string         // 格式: "name@team"
  taskId: string          // 任务追踪 ID
  abortController: AbortController  // 独立的取消控制器
  teammateContext: TeammateContext  // AsyncLocalStorage 上下文
}

// 创建 Teammate 上下文(隔离关键)
export function createTeammateContext(identity: TeammateIdentity) {
  return {
    agentId: identity.agentId,
    agentName: identity.agentName,
    teamName: identity.teamName,
    // ... 其他上下文信息
  }
}

// 在隔离上下文中执行
export async function runWithTeammateContext<T>(
  context: TeammateContext,
  fn: () => Promise<T>
): Promise<T> {
  return teammateAsyncLocalStorage.run(context, fn)
}

// teammateMailbox.ts
// Mailbox 通信(基于文件)
export async function writeToMailbox(
  agentName: string,
  message: { text: string, from: string, color?: string },
  teamName: string,
): Promise<void> {
  const mailboxPath = getMailboxPath(teamName, agentName)
  await fs.appendFile(mailboxPath, JSON.stringify(message) + '\n')
}

export async function readMailbox(
  agentName: string,
  teamName: string,
): Promise<MailboxMessage[]> {
  const mailboxPath = getMailboxPath(teamName, agentName)
  const content = await fs.readFile(mailboxPath, 'utf-8')
  return content.split('\n').filter(Boolean).map(JSON.parse)
}

5. 系统提示词构建

痛点

  • 不同场景需要不同的系统提示(编程助手 vs Coordinator)
  • 用户可能自定义提示
  • 怎么优先级排序?怎么动态注入?

解决方案

分层构建 + 优先级覆盖 + 动态边界标记

  • 5 层优先级:覆盖 > Coordinator > Agent 定义 > 自定义 > 默认
  • 动态边界标记:区分可缓存部分和动态部分
  • 模块化:Intro、System、DoingTasks、Hooks 等独立构建

大白话解释

想象你在写一份"工作手册"给员工:

  • 有的内容所有员工都一样(公司文化、工具使用)
  • 有的内容因岗位而异(程序员 vs 设计师)
  • 有的内容临时调整(今天的特殊任务)

Claude Code 的做法:

  • 先写通用部分(可缓存,省钱)
  • 再写动态部分(每次不同)
  • 用标记分隔,方便系统处理
// systemPrompt.ts
// 系统提示构建(5 层优先级)
export function buildEffectiveSystemPrompt({
  mainThreadAgentDefinition,
  toolUseContext,
  customSystemPrompt,
  defaultSystemPrompt,
  appendSystemPrompt,
  overrideSystemPrompt,
}): SystemPrompt {
  // 优先级 0:完全覆盖
  if (overrideSystemPrompt) {
    return asSystemPrompt([overrideSystemPrompt])
  }
  
  // 优先级 1:Coordinator 模式
  if (isCoordinatorMode()) {
    return asSystemPrompt([getCoordinatorSystemPrompt()])
  }
  
  // 优先级 2:Agent 定义
  const agentSystemPrompt = mainThreadAgentDefinition?.getSystemPrompt()
  
  // 优先级 3:自定义系统提示
  // 优先级 4:默认系统提示
  
  return asSystemPrompt([
    ...(agentSystemPrompt ?? customSystemPrompt ?? defaultSystemPrompt),
    ...(appendSystemPrompt ? [appendSystemPrompt] : []),
  ])
}

// prompts.ts
// 动态边界标记(区分可缓存和动态部分)
export const SYSTEM_PROMPT_DYNAMIC_BOUNDARY =
  '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'

// 构建系统提示的各个部分
function getSimpleIntroSection(): string {
  return `
You are an interactive agent that helps users with software engineering tasks.
Use the instructions below and the tools available to you to assist the user.
`
}

function getSimpleSystemSection(): string {
  return ['# System',
    '- All text you output is displayed to the user.',
    '- Tools are executed in a permission mode.',
    '- The system will automatically compress prior messages.',
  ].join('\n')
}

function getSimpleDoingTasksSection(): string {
  return [
    `Don't add features beyond what was asked.`,
    `Don't create premature abstractions.`,
    `Three similar lines of code is better than a premature abstraction.`,
  ]
}

// 最终组装
export function getSystemPrompt(options: SystemPromptOptions): string[] {
  return [
    getSimpleIntroSection(),
    SYSTEM_PROMPT_DYNAMIC_BOUNDARY,  // 标记:前面可缓存,后面动态
    getSimpleSystemSection(),
    getSimpleDoingTasksSection(),
    // ... 其他部分
  ]
}

6. 安全权限分类

痛点

AI 能执行危险操作(删除文件、执行命令),怎么防止误操作?不能所有操作都问用户(太烦),也不能都不问(太危险)。

解决方案

白名单快速放行 + YOLO 分类器智能判断

  • 白名单:读操作直接放行(不打扰用户)
  • 分类器:两阶段判断(快速 + 深度)
  • 危险操作:必须用户确认

大白话解释

想象你是公司前台(AI),有人(工具)要进办公室:

白名单(直接放行):

  • 送快递的(读文件)
  • 查资料的(搜索)
  • 贴便签的(写待办)

需要判断的:

  • 拿文件的(分类器判断:是不是他本人?)
  • 搬东西的(分类器判断:有没有授权?)

两阶段判断:

  • 第一阶段:快速看一眼(64 tokens),明显安全的直接过
  • 第二阶段:仔细核查(4096 tokens),可疑的深入分析
// classifierDecision.ts
// 安全工具白名单
const SAFE_YOLO_ALLOWLISTED_TOOLS = new Set([
  FILE_READ_TOOL_NAME,      // 读文件 — 安全
  GREP_TOOL_NAME,           // 搜索 — 安全
  GLOB_TOOL_NAME,           // 文件匹配 — 安全
  TODO_WRITE_TOOL_NAME,    // 待办 — 安全
  TASK_CREATE_TOOL_NAME,   // 创建任务 — 安全
  ASK_USER_QUESTION_TOOL_NAME,  // 问用户 — 安全
  // ... 其他安全工具
])

export function isAutoModeAllowlistedTool(toolName: string): boolean {
  return SAFE_YOLO_ALLOWLISTED_TOOLS.has(toolName)
}

// yoloClassifier.ts
// YOLO 分类器(两阶段判断)
async function classifyYoloActionXml(
  tool: string,
  input: unknown,
  context: YoloClassifierContext,
): Promise<YoloClassifierResult> {
  // Stage 1: Fast(快速判断)
  const stage1Opts = {
    model,
    max_tokens: 64,  // 只给 64 tokens,快速返回
    stop_sequences: ['</block>'],
    messages: [
      { role: 'system', content: SYSTEM_PROMPT_STAGE_1 },
      { role: 'user', content: `Tool: ${tool}\nInput: ${JSON.stringify(input)}` }
    ]
  }
  const stage1Raw = await sideQuery(stage1Opts)
  const stage1Block = parseXmlBlock(stage1Raw)
  
  // 如果 Stage 1 说允许,直接放行(快路径)
  if (stage1Block === false) {
    return { 
      shouldBlock: false, 
      reason: 'Allowed by fast classifier' 
    }
  }
  
  // Stage 2: Thinking(深度分析)
  const stage2Opts = {
    model,
    max_tokens: 4096,  // 给更多 tokens,仔细思考
    messages: [
      { role: 'system', content: SYSTEM_PROMPT_STAGE_2 },
      { role: 'user', content: createDetailedPrompt(tool, input, context) }
    ]
  }
  const stage2Raw = await sideQuery(stage2Opts)
  const stage2Result = parseStage2Xml(stage2Raw)
  
  // 返回最终判断
  return {
    shouldBlock: stage2Result.block,
    reason: stage2Result.reasoning,
    thinking: stage2Result.thinking,
  }
}

好的!我来继续写这 3 个模块,保持和之前一样的格式:


7. Token 计数与估算

痛点

  • API 返回的 token 使用量有延迟(异步的)
  • 怎么实时知道当前对话用了多少 tokens?
  • 什么时候该触发压缩?需要精确计算

解决方案

API 响应记录 + 新消息估算 + 实时计算

  • 记录最后一次 API 响应的 token 使用量
  • 估算新消息的 token 数(简单分词)
  • 实时计算当前总使用量

大白话解释

想象你在加油站加油:

  • 油表显示上次加了多少(API 返回)
  • 但你又在开了一段路(新消息)
  • 怎么知道现在还剩多少油?
  • 估算:上次加油量 + 大概消耗 = 当前油量
// tokens.ts
// Token 计数与估算

// 从 API 响应中获取 token 使用量
export function getTokenCountFromUsage(
  usage: Usage | undefined
): TokenCountResult {
  if (!usage) {
    return { inputTokens: 0, outputTokens: 0 }
  }
  
  return {
    inputTokens:
      usage.input_tokens +
      (usage.cache_creation_input_tokens || 0) +
      (usage.cache_read_input_tokens || 0),
    outputTokens: usage.output_tokens || 0,
    cacheReadInputTokens: usage.cache_read_input_tokens,
    cacheCreationInputTokens: usage.cache_creation_input_tokens,
  }
}

// 估算消息的 token 数(简单分词)
export function estimateTokensForMessage(message: Message): number {
  // 简单估算:1 token ≈ 4 个字符(英文)
  const content = getMessageContent(message)
  return Math.ceil(content.length / 4)
}

// 实时计算当前总 token 数
export function tokenCountWithEstimation(messages: Message[]): number {
  // 1. 从最后一条 assistant 消息获取 API 报告的 token 数
  const lastAssistantMessage = findLast(
    messages,
    m => m.role === 'assistant' && m.tokenCount !== undefined
  )
  
  const baseCount = lastAssistantMessage?.tokenCount ?? 0
  
  // 2. 估算 baseCount 之后的新消息
  const newMessages = messages.slice(
    messages.indexOf(lastAssistantMessage) + 1
  )
  
  const estimatedNewTokens = newMessages.reduce(
    (sum, msg) => sum + estimateTokensForMessage(msg),
    0
  )
  
  // 3. 返回总和
  return baseCount + estimatedNewTokens
}

// 获取当前使用量(用于判断是否触发压缩)
export function getCurrentUsage(messages: Message[]): TokenUsage {
  const count = tokenCountWithEstimation(messages)
  const threshold = getAutoCompactThreshold(getCurrentModel())
  
  return {
    used: count,
    threshold: threshold,
    remaining: threshold - count,
    percentage: (count / threshold) * 100,
  }
}

8. 工具调用链路(Tool Use Loop)

痛点

  • AI 说"我要调用 Bash 工具执行 ls",怎么执行?
  • 执行完怎么把结果返回给 AI?
  • AI 又想调用另一个工具,循环什么时候结束?

解决方案

请求-执行-响应循环 + 工具注册 + 结果组装

  • 解析 AI 的工具调用请求
  • 查找并执行对应工具
  • 把结果格式化为消息返回给 AI
  • 循环直到 AI 不再调用工具

大白话解释

想象你是餐厅服务员(Agent):

  1. 顾客(AI)说:“我要点牛排”(调用工具)
  2. 你记下来,传给厨房(执行工具)
  3. 厨房做好,你端给顾客(返回结果)
  4. 顾客又说:“再来杯红酒”(再次调用)
  5. 重复直到顾客说"我吃饱了"(停止调用)
// AgentTool/runAgent.ts
// 工具调用主循环

export async function runAgent(options: RunAgentOptions): Promise<AgentResult> {
  const { messages, tools, systemPrompt, maxTurns = 50 } = options
  
  let currentMessages = [...messages]
  let turnCount = 0
  
  // 主循环:最多 maxTurns 轮
  while (turnCount < maxTurns) {
    turnCount++
    
    // 1. 调用 LLM,获取响应
    const response = await callLLM({
      model: getCurrentModel(),
      system: systemPrompt,
      messages: currentMessages,
      tools: tools.map(t => t.definition),  // 工具定义
    })
    
    // 2. 检查是否有工具调用
    const toolUses = extractToolUses(response.content)
    
    // 如果没有工具调用,直接返回结果
    if (toolUses.length === 0) {
      return {
        finalMessage: response.content,
        messages: currentMessages,
        usage: response.usage,
      }
    }
    
    // 3. 执行工具调用
    const toolResults: ToolResult[] = []
    
    for (const toolUse of toolUses) {
      // 查找对应工具
      const tool = tools.find(t => t.name === toolUse.name)
      
      if (!tool) {
        toolResults.push({
          tool_use_id: toolUse.id,
          content: `Error: Unknown tool ${toolUse.name}`,
          is_error: true,
        })
        continue
      }
      
      // 执行工具
      try {
        const result = await tool.execute(toolUse.input, toolUseContext)
        toolResults.push({
          tool_use_id: toolUse.id,
          content: result,
          is_error: false,
        })
      } catch (error) {
        toolResults.push({
          tool_use_id: toolUse.id,
          content: `Error: ${error.message}`,
          is_error: true,
        })
      }
    }
    
    // 4. 组装结果,添加到消息历史
    currentMessages.push(
      // AI 的工具调用请求
      createAssistantMessage({
        content: response.content,
      }),
      // 工具执行结果
      createUserMessage({
        content: toolResults.map(r => ({
          type: 'tool_result',
          tool_use_id: r.tool_use_id,
          content: r.content,
          is_error: r.is_error,
        })),
      })
    )
    
    // 5. 继续循环(让 AI 看到结果,决定下一步)
  }
  
  // 超过最大轮数,强制结束
  throw new Error(`Exceeded maximum turns (${maxTurns})`)
}

// 工具定义示例
export interface Tool {
  name: string
  description: string
  parameters: JSONSchema
  execute: (input: unknown, context: ToolUseContext) => Promise<string>
}

// Bash 工具实现示例
export const BashTool: Tool = {
  name: 'bash',
  description: 'Execute bash commands',
  parameters: {
    type: 'object',
    properties: {
      command: { type: 'string' },
      timeout: { type: 'number' },
    },
    required: ['command'],
  },
  async execute(input, context) {
    const { command, timeout = 60000 } = input as { command: string; timeout?: number }
    
    // 权限检查
    if (!await checkPermission(context, 'bash', command)) {
      throw new Error('Permission denied')
    }
    
    // 执行命令
    const result = await exec(command, { timeout })
    return result.stdout
  },
}

9. 会话状态持久化

痛点

  • 程序崩溃了,对话历史怎么恢复?
  • 用户下次打开怎么继续上次的对话?
  • 多设备怎么同步?

解决方案

定期保存 + 增量写入 + 会话恢复

  • 每轮对话后保存到本地文件
  • 增量写入,避免重复保存
  • 启动时自动检测并恢复会话

大白话解释

想象你在写 Word 文档:

  • 自动保存:每隔几分钟保存一次
  • 增量保存:只保存修改的部分,不是整个文件
  • 恢复:崩溃后打开,提示"是否恢复未保存的文档?"
// sessionStorage.ts
// 会话状态持久化

// 会话数据接口
export interface SessionData {
  sessionId: string
  messages: Message[]
  fileState: FileStateCache
  todoState: TodoState
  contextWindow: number
  lastSavedAt: number
}

// 保存会话
export async function saveSession(data: SessionData): Promise<void> {
  const sessionPath = getSessionPath(data.sessionId)
  
  // 序列化并写入文件
  const serialized = JSON.stringify(data, null, 2)
  await fs.writeFile(sessionPath, serialized, 'utf-8')
  
  // 同时保存到备份路径(防止写入失败)
  const backupPath = getBackupPath(data.sessionId)
  await fs.writeFile(backupPath, serialized, 'utf-8')
  
  logEvent('session_saved', {
    sessionId: data.sessionId,
    messageCount: data.messages.length,
  })
}

// 加载会话
export async function loadSession(sessionId: string): Promise<SessionData | null> {
  try {
    const sessionPath = getSessionPath(sessionId)
    const content = await fs.readFile(sessionPath, 'utf-8')
    return JSON.parse(content) as SessionData
  } catch (error) {
    // 主文件损坏,尝试从备份恢复
    try {
      const backupPath = getBackupPath(sessionId)
      const content = await fs.readFile(backupPath, 'utf-8')
      logEvent('session_recovered_from_backup', { sessionId })
      return JSON.parse(content) as SessionData
    } catch {
      return null
    }
  }
}

// 自动保存钩子
export function useAutoSave(
  appState: AppState,
  intervalMs: number = 30000  // 默认 30 秒
): void {
  useEffect(() => {
    const interval = setInterval(() => {
      if (appState.messages.length > 0) {
        saveSession({
          sessionId: appState.sessionId,
          messages: appState.messages,
          fileState: appState.fileState,
          todoState: appState.todoState,
          contextWindow: appState.contextWindow,
          lastSavedAt: Date.now(),
        })
      }
    }, intervalMs)
    
    return () => clearInterval(interval)
  }, [appState])
}

// 会话恢复
export async function restoreSession(
  sessionId?: string
): Promise<SessionData | null> {
  // 1. 如果指定了 sessionId,尝试加载
  if (sessionId) {
    return loadSession(sessionId)
  }
  
  // 2. 否则查找最近的活动会话
  const recentSessions = await listRecentSessions()
  if (recentSessions.length > 0) {
    const mostRecent = recentSessions[0]
    const data = await loadSession(mostRecent.sessionId)
    
    if (data) {
      // 询问用户是否恢复
      const shouldRestore = await askUser({
        message: `发现未完成的会话(${formatTime(data.lastSavedAt)}),是否恢复?`,
        choices: ['恢复', '放弃'],
      })
      
      if (shouldRestore === '恢复') {
        return data
      }
    }
  }
  
  return null
}

// 清理旧会话
export async function cleanupOldSessions(maxAgeDays: number = 7): Promise<void> {
  const sessions = await listAllSessions()
  const now = Date.now()
  const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000
  
  for (const session of sessions) {
    if (now - session.lastSavedAt > maxAgeMs) {
      await deleteSession(session.sessionId)
      logEvent('session_cleaned_up', {
        sessionId: session.sessionId,
        ageDays: (now - session.lastSavedAt) / (24 * 60 * 60 * 1000),
      })
    }
  }
}

10. 错误处理与重试

痛点

  • API 调用失败了怎么办?
  • 网络超时怎么办?
  • 怎么优雅降级,不让用户看到崩溃?

解决方案

分层重试 + 指数退避 + 优雅降级

  • 网络错误:自动重试 3 次,指数退避
  • API 限流:等待后重试
  • 严重错误:降级到备用模型或提示用户

大白话解释

想象你在打电话:

  • 第一次没打通:等 1 秒再打
  • 第二次没打通:等 2 秒再打
  • 第三次没打通:等 4 秒再打
  • 还是不通:发短信说"稍后回电"(降级)
// apiRetry.ts
// API 重试机制

// 可重试的错误类型
const RETRYABLE_ERRORS = [
  'ECONNRESET',      // 连接重置
  'ETIMEDOUT',       // 超时
  'ECONNREFUSED',    // 连接拒绝
  'RATE_LIMIT',      // 限流
  'SERVICE_UNAVAILABLE',  // 服务不可用
]

// 重试配置
interface RetryConfig {
  maxRetries: number      // 最大重试次数
  baseDelayMs: number     // 基础延迟
  maxDelayMs: number      // 最大延迟
  backoffMultiplier: number  // 退避倍数
}

const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 3,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  backoffMultiplier: 2,
}

// 带重试的 API 调用
export async function callWithRetry<T>(
  fn: () => Promise<T>,
  config: Partial<RetryConfig> = {}
): Promise<T> {
  const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config }
  let lastError: Error
  
  for (let attempt = 0; attempt <= fullConfig.maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error as Error
      
      // 判断是否应该重试
      if (!shouldRetry(error)) {
        throw error  // 不可重试,直接抛出
      }
      
      // 最后一次尝试,不再重试
      if (attempt === fullConfig.maxRetries) {
        break
      }
      
      // 计算退避时间(指数退避 + 随机抖动)
      const delay = calculateDelay(attempt, fullConfig)
      
      logEvent('api_retry_scheduled', {
        attempt: attempt + 1,
        maxRetries: fullConfig.maxRetries,
        delayMs: delay,
        error: error.message,
      })
      
      // 等待后重试
      await sleep(delay)
    }
  }
  
  // 所有重试都失败
  throw new RetryExhaustedError(
    `Failed after ${fullConfig.maxRetries} retries`,
    lastError!
  )
}

// 判断是否应该重试
function shouldRetry(error: unknown): boolean {
  if (error instanceof APIError) {
    // 5xx 错误和限流可重试
    if (error.status >= 500 || error.status === 429) {
      return true
    }
    
    // 检查错误代码
    if (RETRYABLE_ERRORS.includes(error.code)) {
      return true
    }
  }
  
  // 网络错误可重试
  if (error instanceof NetworkError) {
    return true
  }
  
  return false
}

// 计算退避时间
function calculateDelay(
  attempt: number,
  config: RetryConfig
): number {
  // 指数退避:1s, 2s, 4s...
  const exponentialDelay = config.baseDelayMs * 
    Math.pow(config.backoffMultiplier, attempt)
  
  // 加上随机抖动(0-1s),避免 thundering herd
  const jitter = Math.random() * 1000
  
  // 不超过最大延迟
  return Math.min(exponentialDelay + jitter, config.maxDelayMs)
}

// 使用示例
export async function callLLMWithRetry(
  params: LLMParams
): Promise<LLMResponse> {
  return callWithRetry(
    () => callLLM(params),
    {
      maxRetries: 3,
      baseDelayMs: 1000,
    }
  )
}

// 优雅降级
export async function callLLMWithFallback(
  params: LLMParams
): Promise<LLMResponse> {
  try {
    // 先尝试主模型
    return await callLLMWithRetry({
      ...params,
      model: getPrimaryModel(),
    })
  } catch (error) {
    logEvent('primary_model_failed', { error: error.message })
    
    // 主模型失败,降级到备用模型
    try {
      return await callLLMWithRetry({
        ...params,
        model: getFallbackModel(),  // 更便宜的模型
        maxTokens: Math.floor((params.maxTokens || 4000) / 2),  // 减少输出
      })
    } catch (fallbackError) {
      // 备用模型也失败,提示用户
      throw new Error(
        '服务暂时不可用,请稍后重试。' +
        '如果问题持续,请检查网络连接或联系支持。'
      )
    }
  }
}

第三部分:总结:“去掉大模型的“神话滤镜”,回归软件工程的本质”

这场轰动全网的 Claude Code 源码泄露事故,不仅让我们吃了一个“前端配置失误”的惊天大瓜,更像是一本免费公开的**“工业级 Agent 避坑指南”**。

通过深度研读这份价值连城的源码,我们不难发现一个残酷而真实的工程铁律:即使是目前地表最强的大模型(如 Claude 4.6),在面对复杂的真实业务链条时,它依然是一个充满随机性和不可控风险的“黑盒”。

一个真正能商用落地的 AI Agent,绝不是靠写几句精妙绝伦的 Prompt(提示词)就能包打天下的。大模型决定了系统的“上限”有多高,而我们写的这些传统工程代码,决定了系统的“下限”有多稳。

不管是遇到底层依赖缺失时的优雅降级(Graceful Degradation),还是面对模型死循环时的**硬性代码熔断与退避(Circuit Breaker & Backoff),这些看似笨拙的传统软件工程防线,才是压舱石。

对于我们开发者而言,盲目迷信大模型自身的“全知全能”是非常危险的。学会用确定性的代码架构,去驾驭和圈禁不确定性的 AI,才是大模型时代最有价值的护城河。

如果这篇文章对你有启发,欢迎点赞、收藏、评论交流!后续我会分享更多实战踩坑经验,敬请期待!


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐