欢迎光临
我们一直在努力

流式、幂等、安全:QueryEngine 的工程保障三件套

流式、幂等、安全:QueryEngine 的工程保障三件套

《Claude Code 架构解密》读书笔记 · 第04篇 对应章节:第3章后半(3.6-3.13)— 查询处理的工程保障


导语

上一篇我们拆解了 QueryEngine 的"骨架"——while(true) 状态机、二元分离、AsyncGenerator、配置快照。但骨架只是起点。一个裸的 while(true) 不崩,靠的不是运气,而是一整套工程保障体系:八步管线管住查询生命周期、四层压缩管线管住上下文膨胀、分级错误恢复管住异常路径、窄依赖注入管住测试边界。

本篇从"骨架"走进"器官",看 Claude Code 如何让死循环真正可靠地跑起来。


一、查询生命周期全景:submitMessage 八步管线

从用户输入到结果返回

一次完整的查询从 QueryEngine.submitMessage() 开始,经过八个阶段:

用户输入 "帮我重构 parsePath 函数"


① 初始化:清理技能集合、设置 cwd、记录时间戳


② 权限包装:将 canUseTool 包装为带拒绝追踪的版本
(每次权限拒绝都被记录,用于后续分析)


③ 系统提示构建:fetchSystemPromptParts() → asSystemPrompt()
(动态组装系统提示,详见第7章)


④ 用户输入处理:processUserInput()
(解析斜杠命令、处理附件、展开粘贴引用)


⑤ 消息持久化:recordTranscript()
(将用户消息写入 JSONL 会话记录)


⑥ 查询执行:调用 query() 异步生成器
queryLoop() while(true)
→ 上下文压缩管线(§3.7 详述)
→ API 调用(流式响应)
→ 工具执行(流式或批量)
→ 错误恢复
→ Stop Hooks 执行
→ Token 预算检查


⑦ 后处理:更新 totalUsage、技能发现结果等会话级状态


⑧ 返回结果:流式产出 SDKMessage 给调用方

装饰器式的权限追踪

第②步的设计细节值得关注。canUseTool 是权限检查函数(详见第5章),但 QueryEngine 不是直接传递,而是用包装器追踪权限拒绝:

const wrappedCanUseTool = async (tool, input) => {
const result = await canUseTool(tool, input)
if (result.denied) {
this.permissionDenials.push({
tool: tool.name,
input,
reason: result.reason,
timestamp: Date.now()
})
}
return result
}

装饰器模式让权限追踪逻辑不侵入 canUseTool 的实现,也不污染 query() 的代码——关注点分离的又一个实例。

State 对象的函数式更新

queryLoop 中的状态更新采用函数式不可变更新:

state = {
state, // 保留所有旧字段
messages: [state.messages, newMsg], // 追加新消息
turnCount: state.turnCount + 1, // 递增轮次
transition: { reason: 'next_turn' } // 设置转换原因
}

{…state, …updates} 的展开赋值有三个优势:

  • 可追踪性:每次状态变化都是新对象,可在循环顶部设断点对比前后状态
  • 与 React 哲学一致:Claude Code 的 UI 层使用 React (Ink),函数式状态更新是其核心范式
  • 可预测性:不存在"意外修改旧状态"的可能
  • 但注意——这种"函数式"是有限度的。state.messages 数组本身是可变的(push() 操作),因为完全不可变的消息数组在频繁追加时会产生大量 GC 压力。务实 Trade-off:状态顶层保持不可变语义,热点路径上允许可变操作。


    二、四层压缩管线:先轻后重的上下文管理

    根本矛盾:对话越有用,离上下文极限越近

    长会话 Agent 面临一个根本性矛盾:

    • 读取 20+ 个文件(每个几百到几千行)
    • 执行 10+ 次编辑操作
    • 运行 5+ 次测试命令

    一个 cat 命令输出 2000 行文件,就可能消耗 8000+ tokens。当消息历史膨胀到接近 200K 上下文窗口时,API 返回 413 错误(Prompt Too Long)。

    直觉方案是"压缩"——用 LLM 生成摘要替代历史。但有两个问题:

  • 成本高:压缩一次的成本几乎等于再问一个问题
  • 信息损失:摘要省略的细节可能导致 Agent 重复读取文件
  • 四层递进策略

    Claude Code 的答案:不是所有膨胀都需要 LLM 介入。整个压缩体系由 12 个文件、约 3900 行代码组成,构建了四层递进策略:

    第一层 第二层 第三层 第四层
    API 原生 → 微压缩 → Session Memory → Full Compact
    零成本 零 LLM 成本 零 LLM 成本 LLM 驱动
    服务端清理 时间/缓存触发 后台笔记替代摘要 结构化摘要

    轻量/快速 ──────────────────────────────────────→ 重量/慢速
    信息保留多 ──────────────────────────────────────→ 信息保留少

    "先轻后重"确保大多数情况只需轻量压缩,只有当轻量手段不足时才启用更"重"的策略。

    第一层:API 原生上下文管理(零成本)

    利用 Anthropic API 的原生 context_management 参数,让服务端自动清理工具结果和 thinking 块,客户端零开销:

    type ContextEditStrategy = {
    type: 'clear_tool_uses_20250919'
    trigger: { type: 'input_tokens'; value: 180_000 } // 输入超 180K 时触发
    keep: { type: 'tool_uses'; value: number } // 保留最近 N 个
    exclude_tools: string[] // 排除写操作工具
    }

    关键设计:并非所有工具结果都可以安全清理。

    可清理(只读)不可清理(写操作)
    Bash、Glob、Grep、Read、WebFetch、WebSearch Edit、Write、NotebookEdit
    结果是信息性的,清理后可重新执行 代表"做过的修改",清理后模型丧失记忆

    第二层:微压缩(零 LLM 成本)

    两条子路径,都不涉及 LLM 调用:

    路径 A:时间触发微压缩

    核心洞察:Anthropic 的 Prompt Cache TTL 约 1 小时。用户离开超 60 分钟再回来,缓存必然已过期,清理旧工具结果不会造成额外缓存 miss。

    // 触发条件:距上次 assistant 消息超过 60 分钟
    if (timeSinceLastAssistant > 60 * 60 * 1000) {
    const compactableIds = collectCompactableToolIds(messages)
    const toClean = compactableIds.slice(0, 5) // 保留最近 5 个
    for (const id of toClean) {
    replaceToolResult(id, "[old tool result content cleared]")
    }
    }

    路径 B:缓存微压缩(Cached Microcompact)

    利用 API 的 cache_edits 能力,在服务端删除工具结果,不修改本地消息。这样既释放上下文空间,又保留本地消息完整性(对会话记录和调试有价值),更不会破坏 cache prefix 的有效性。

    工程细节:只有主线程可以注册 cached microcompact 状态。子 Agent 与主线程共享进程和模块级状态,如果子 Agent 错误地修改了主线程的缓存状态,会导致难以追踪的 bug:

    if (!isMainThreadSource(querySource)) return { messages }

    第三层:Session Memory 压缩(零 LLM 成本,创新性设计)

    这是四层管线中最具创新性的设计。

    传统方式是"事后摘要"——上下文快满时调用 LLM 生成摘要。Claude Code 引入了"实时笔记"策略:后台有一个独立的 session-memory extraction agent 持续运行,像勤勉的会议记录员,不断将对话中的关键信息提取为结构化笔记。需要压缩时,直接用已有笔记替代 LLM 摘要——零 LLM 成本。

    笔记(SessionMemory)被组织为九个章节:

  • Primary Request and Intent——用户核心目标
  • Key Technical Concepts——涉及的技术概念
  • Files and Code Sections——操作过的文件和代码段
  • Errors and Fixes——遇到的错误和修复
  • Problem Solving——问题分析过程
  • All User Messages——所有用户消息
  • Pending Tasks——未完成的任务
  • Current Work——当前进行的工作
  • Optional Next Step——建议的下一步
  • 保留多少近期消息? 这是一个双约束优化问题:

    // calculateMessagesToKeepIndex 的核心逻辑
    // 向前扩展,直到同时满足:
    1. 保留的 token 数 ≥ minTokens(10K) // 确保足够的近期上下文
    2. 保留的文本块消息数 ≥ minTextBlockMessages(5// 确保对话连贯性
    3. token 数 ≤ maxTokens(40K) // 防止保留太多(硬上限)
    → adjustIndexToPreserveAPIInvariants // 确保不破坏 API 约束

    adjustIndexToPreserveAPIInvariants 是整个 Compact 系统中最精细的边界处理:

    • Step 1:工具配对完整性——API 要求每个 tool_result 都有对应的 tool_use。分割点恰好在两者之间时,必须向前扩展保留范围
    • Step 2:Thinking 块连续性——同一 API 响应的 thinking 块和文本块共享 message.id,分割点不能拆开同一响应

    这个算法保证了一个关键不变式:压缩后的消息序列对 API 来说仍然是合法的。tool_use/tool_result 配对、thinking 块连续性、消息角色交替——任何一个被破坏都会导致 API 返回 400 错误。

    第四层:Full Compact——LLM 驱动的结构化摘要

    当前三层都不够时,最重的手段被启用。采用双路径策略:

    优先路径:ForkedAgent 路径(缓存共享)

    • 复用主对话的 system prompt → 命中 prompt cache → ~30% 成本折扣
    • skipCacheWrite: true → 不覆盖主对话的缓存
    • maxTurns: 1 → 只允许一轮,防止压缩 Agent 自己去调用工具

    回退路径:独立 API 调用

    • 独立的 API 调用
    • 消息预处理:剥离图片、移除重注入的附件
    • 支持 2 次 PTL 重试

    Prompt 设计的防御性工程:压缩 Agent 继承了父进程的完整工具集(为了 cache key 匹配),但它不应该调用任何工具。Prompt 中包含了防线:

    CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
    – Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
    – Tool calls will be REJECTED and will waste your only turn – you will fail the task.

    即便如此,测试中发现模型仍有约 2.79% 的概率尝试工具调用。maxTurns: 1 是第二道防线——即使模型尝试调用工具,也只有一轮机会。

    摘要输出采用 <analysis> + <summary> 双段设计。<analysis> 块在后处理时被剥离——作用是让模型"先思考再总结"(scratchpad thinking),最终只有 <summary> 部分被注入对话历史,避免浪费上下文空间。

    压缩触发:自动决策引擎

    autoCompact.ts(约 351 行)是整个系统的"大脑":

    阈值计算:
    effectiveWindow = contextWindowForModel – maxOutputTokensForModel
    autoCompactThreshold = effectiveWindow – AUTOCOMPACT_BUFFER_TOKENS(13_000)

    示例:200K 上下文窗口,8K 最大输出
    effectiveWindow = 200K – 8K = 192K
    autoCompactThreshold = 192K – 13K = 179K
    → 当输入 token 超过 179K 时,触发自动压缩

    五层防护确保压缩不会导致递归或竞争:

  • 互斥锁:防止"压缩操作触发新的压缩"的无限循环
  • 功能开关:支持通过环境变量或用户配置禁用自动压缩
  • Reactive Compact 互斥:如果响应式压缩已在进行,主动压缩退让
  • Context Collapse 互斥:避免两种压缩策略同时操作消息历史
  • 熔断器(Circuit Breaker):连续压缩失败 3 次后静默停止,防止无限浪费 API 调用
  • 策略优先级:

  • Session Memory Compact(零 LLM 成本,最优选)→ 失败 → 进入下一级
  • Full Compact(LLM 驱动,兜底方案)→ 失败 → 递增 consecutiveFailures → 可能触发熔断器
  • 消息分组算法:API Round 分组

    压缩操作中一个棘手的问题:如何分割消息历史?不能在任意位置切割——tool_use 和 tool_result 必须成对,thinking 块必须连续。

    旧方案(按"人类输入"分组)的问题:在 SDK/CCR/eVal 等单轮 agentic 场景下,一个"人类轮次"可能包含数百次工具调用,无法有效分割。

    新方案(按 API Round 分组):以 assistant.message.id 的变化作为边界:

    for (const msg of messages) {
    if (msg.type === 'assistant' &&
    msg.message.id !== lastAssistantId &&
    current.length > 0) {
    groups.push(current) // 新 message.id → 新的 API 轮次
    current = [msg]
    } else {
    current.push(msg)
    if (msg.type === 'assistant') {
    lastAssistantId = msg.message.id
    }
    }
    }

    使用 API 返回的 message.id 而非内部生成的 UUID,是因为同一流式响应中的多条消息共享同一个 message.id——这恰好是"API 轮次"的自然边界。

    后压缩清理:被忽视的关键环节

    压缩完成后,还需清理一系列全局状态——容易被忽视但极其重要:

    // 无条件重置
    resetMicrocompactState() // 微压缩追踪状态
    clearSystemPromptSections() // 系统提示区段缓存
    clearClassifierApprovals() // bash 分类器批准记录
    clearSpeculativeChecks() // 推测性权限检查
    clearBetaTracingState() // 遥测追踪
    clearSessionMessagesCache() // session 存储缓存

    // 仅主线程
    if (isMainThreadCompact) {
    getUserContext.cache.clear() // 用户上下文 memoize 缓存
    resetGetMemoryFilesCache('compact') // memory 文件 one-shot 标志
    // 注意:故意不重置 sentSkillNames
    // 原因:skill 内容需在多次压缩间保持,省去每次重注入 ~4K tokens
    }

    精华所在:主线程 vs 子 Agent 的区分。子 Agent 与主线程共享进程和模块级状态(Node.js 特性),子 Agent 压缩时错误清理主线程状态会导致难以追踪的 bug——比如用户突然被重新要求授权已批准的操作。

    sentSkillNames 的"故意不重置"是有意思的反模式——通常我们期望压缩后状态是"干净"的,但为了节省 4K tokens 的重注入成本,特意保留。清理规则不是"全部重置"或"全部保留"的二元选择,需要逐个字段评估保留价值。


    三、分级错误恢复:每种错误配独立药方

    为什么不能"出错就重试"?

    queryLoop 执行中各种错误随时可能发生。不同错误的恢复方向可能完全相反:

    错误恢复方向
    Prompt Too Long (413) 减少输入——压缩历史
    Max Output Tokens 增加输出——升级 maxOutputTokens
    Model Unavailable 换模型——降级到备用模型
    Media Size Error 剥离媒体——移除图片

    如果对四种错误都执行"减少输入",Max Output Tokens 的恢复反而会让情况更糟。

    分级恢复策略

    错误类型恢复策略限制
    Prompt Too Long (413) Context Collapse → Reactive Compact 各一次
    Max Output Tokens 升级 64K maxOutputTokens → 多轮恢复消息 3 次
    Model Unavailable 切换到 Fallback 模型 1 次
    Media Size Error Reactive Compact + 图片剥离 1 次

    最复杂的路径——Prompt Too Long 两步恢复:

    API 返回 413(Prompt Too Long)

    ├─ 尝试 1: Context Collapse
    │ 将历史消息折叠为摘要,保留近期消息
    │ transition = 'collapse_drain_retry'
    │ ├─ 成功 → continue(回到循环顶部重试 API 调用)
    │ └─ 失败(仍然 413)→ 进入尝试 2

    └─ 尝试 2: Reactive Compact
    更激进的压缩——调用 LLM 生成完整摘要
    transition = 'reactive_compact_retry'
    ├─ 成功 → continue
    └─ 失败 → 终止循环,返回错误

    注意:transition 字段防止重复尝试
    如果 transition 已经是 'collapse_drain_retry'
    → 跳过 Context Collapse,直接进入 Reactive Compact

    Max Output Tokens 的渐进式升级:

    模型输出被截断(达到 maxOutputTokens 限制)

    ├─ 恢复 1:升级 maxOutputTokens 到 64K
    │ 设置 maxOutputTokensOverride = 64_000
    │ 发送恢复消息:"你的输出被截断了,请继续"
    │ continue

    ├─ 恢复 2:再次截断 → 再发恢复消息
    │ maxOutputTokensRecoveryCount++
    │ continue

    └─ 恢复 3:第三次截断 → 终止
    "已尝试 3 次恢复,无法继续"

    错误恢复作为状态转换

    将错误恢复与 while(true) 状态机结合,产生了一个简洁的实现模式:

    if (error.type === 'prompt_too_long') {
    if (!state.hasAttemptedReactiveCompact) {
    const compacted = await reactiveCompact(state.messages)
    state = {
    state,
    messages: compacted,
    hasAttemptedReactiveCompact: true,
    transition: { reason: 'reactive_compact_retry' }
    }
    continue // ← 回到循环顶部,用压缩后的消息重试
    }
    // 已尝试过压缩,无法恢复
    return { reason: 'error', error }
    }

    优雅之处:错误恢复不是特殊的代码路径,而是状态机的一个正常转换。压缩后重试和正常工具调用后继续,在代码结构上完全对称——都是"修改 state → 设置 transition → continue"。


    四、依赖注入:四个依赖的精准边界

    为什么不用 DI 框架?

    query() 需要调用外部服务——LLM API、压缩服务、UUID 生成器。测试时需要 mock。query() 接受可选的 deps 参数:

    type QueryDeps = {
    callModel: typeof callModel // LLM API 调用
    microcompact: typeof microcompact // 微压缩
    autocompact: typeof autocompact // 自动压缩
    uuid: typeof uuid // UUID 生成
    }

    // 生产环境使用真实实现
    function productionDeps(): QueryDeps {
    return { callModel, microcompact, autocompact, uuid }
    }

    // 测试中可以注入 mock
    const testDeps: Partial<QueryDeps> = {
    callModel: mockCallModel,
    uuid: () => 'test-uuid-001'
    }

    只为 4 个 mock 点引入 InversifyJS/tsyringe 等 DI 框架——装饰器语法、容器配置、运行时类型检查——是典型的过度工程。

    typeof 的类型同步技巧

    callModel: typeof callModel // 类型自动与 callModel 函数签名同步

    当 callModel 的签名变化时(比如新增参数),QueryDeps 的类型定义会自动更新——不需要手动维护两份签名。如果使用传统接口定义,原函数签名变化时很容易遗漏更新 mock 接口。

    渐进式扩展

    代码注释中列出了可能在未来添加的依赖:runTools、handleStopHooks、logEvent。但当前没包含,因为测试不需要 mock 它们——对工具执行测试,Claude Code 使用 spyOn 而非依赖注入。

    这不是理想做法,但体现了一个务实原则:先解决当前痛点(4 个核心 I/O 依赖),而不是一次性设计"完美" DI 方案。这就是渐进式依赖注入的精髓:从最小 mock 集合开始,按需扩展。


    五、子模块的单一职责

    query() 的逻辑被拆分到四个子模块,每个维护严格的单一职责:

    输入模块职责输出副作用
    全局状态、环境变量 config.ts 配置快照 QueryConfig(纯数据)
    依赖注入边界 deps.ts 依赖定义 QueryDeps(函数接口)
    Token 预算 tokenBudget.ts 预算决策 TokenBudgetDecision + token 计数 修改 tracker
    状态 + 消息 + 上下文 stopHooks.ts Hook 编排 StopHookResult + yield 消息 触发后台任务

    注意"副作用"列的差异:config.ts 和 deps.ts 是纯函数(无副作用),tokenBudget.ts 和 stopHooks.ts 有副作用。这种分离不是偶然的——将可以纯化的部分拆出来,使其更接近 (state, event, config) → (state, output) 的 reducer 形态,为未来可能的重构(提取 step() 函数使 queryLoop 可被单步测试)做准备。

    tokenBudget.ts:预算决策引擎

    Token 预算是 Claude Code 控制成本的关键机制,做出三种决策之一:

    type TokenBudgetDecision =
    | { action: 'continue' } // 预算充足,继续
    | { action: 'stop' } // 预算耗尽,终止
    | { action: 'warn_and_continue' } // 接近上限,继续但发出警告

    stopHooks.ts:用户定义的生命周期拦截

    三种 Hook 类型:

    • Stop:Agent 完成当前任务时触发
    • TaskCompleted:任务明确完成时触发
    • TeammateIdle:多 Agent 协作中队友空闲时触发

    每种 Hook 都支持两种控制语义:

    • blocking error:Hook 返回错误,阻止 Agent 停止,要求处理错误
    • preventContinuation:阻止 Agent 自动继续,强制等待用户输入

    这个设计让用户可以实现诸如"Agent 完成修改后自动运行测试,测试失败则要求 Agent 修复"的工作流(详见第9章)。


    六、横向对比

    Claude Code vs LangChain/LangGraph

    维度Claude CodeLangChain/LangGraph
    循环模式 while(true) + 隐式状态机 显式 Graph(节点+边)
    工具执行 流式+批量双模式 通常批量执行
    上下文管理 四层压缩管线 通常依赖外部 Memory
    权限控制 内建分层权限模型 无内建权限
    错误恢复 分级恢复策略(每种错误独立路径) 通常简单重试
    状态更新 函数式 {…state, …updates} Graph 节点间传递状态

    LangGraph 用显式有向图定义状态转换,可视化和理解上有优势。但 Claude Code 的转换路径以线性为主,不需要图结构的表达能力。

    Claude Code vs OpenAI Assistants API

    维度Claude CodeOpenAI Assistants API
    运行位置 客户端(本地) 服务端
    状态管理 客户端内存+本地持久化 服务端 Thread
    工具执行 本地执行 Function Calling → 客户端执行 → 提交结果
    上下文压缩 客户端主动管理(四层管线) 服务端透明处理
    取消机制 AsyncGenerator.return() Cancel Run API

    两种架构流派——“胖客户端"和"胖服务端”。Claude Code 将查询循环放在客户端,获得完全控制权(流式工具执行、本地权限检查、sandbox 隔离),但代价是客户端需要自行管理所有状态和压缩。OpenAI Assistants 将状态放在服务端,简化了客户端,但牺牲了细粒度控制。


    七、六大可复用设计模式

    从本章提炼出六个可复用模式:

    模式 1:Generator 驱动的查询循环

    • 问题:Agent 系统需要流式输出中间结果,同时支持取消和背压
    • 方案:AsyncGenerator 驱动循环,yield 输出中间结果,return() 支持取消
    • 适用:需要流式输出、消费者需控制速率
    • 不适用:所有结果可一次性返回

    模式 2:级联压缩管线

    • 问题:上下文窗口有限,需在保留信息和释放空间间取得平衡
    • 方案:多层递进压缩策略,按"先轻后重"排列,每层失败后优雅降级
    • 适用:有多种压缩手段(成本不同)、大多数情况只需轻量压缩
    • 不适用:所有情况都需要同等深度的压缩

    模式 3:配置快照

    • 问题:长时间异步操作中,运行时配置可能变化,导致行为不一致
    • 方案:操作开始时一次性快照配置,操作期间使用快照值
    • 适用:操作持续秒到分钟级、可复现性比实时性更重要
    • 不适用:需要实时响应配置变更

    模式 4:窄依赖注入边界

    • 问题:核心代码需调用外部服务,测试需 mock,但 DI 框架过重
    • 方案:手动定义最小依赖接口 + 工厂函数,只注入核心 I/O 边界
    • 适用:mock 点数量少(< 10 个)
    • 不适用:需要大规模依赖注入(100+ 注入点)

    模式 5:轮次状态机

    • 问题:需要状态机但转换路径简单,不值得引入框架
    • 方案:while(true) + State 对象 + transition 字段,通过 continue/return 控制流转
    • 适用:转换路径线性为主(约 5-10 种 reason)、需与 AsyncGenerator 兼容
    • 不适用:状态转换复杂(图结构、条件分支合并)

    模式 6:分级错误恢复

    • 问题:不同错误类型需不同恢复策略,统一重试效率低下
    • 方案:为每种错误类型设计独立恢复路径和次数限制
    • 适用:错误类型多且恢复方向不同、每种恢复有明确次数上限
    • 不适用:所有错误恢复策略相同

    八、实战启示

    启示一:压缩先轻后重,恢复逐级升级

    四层压缩管线和分级错误恢复遵循同一个元模式——渐进式降级。先尝试零成本的方案,失败后才逐步升级到更重的手段。这个模式适用于任何"多种策略可选但成本不同"的场景——缓存策略、降级策略、重试策略。

    启示二:后压缩清理比压缩本身更危险

    压缩逻辑容易想到,但压缩后的状态清理常被忽视。Claude Code 的经验表明:清理时最大的陷阱是共享进程状态(主线程 vs 子 Agent)。任何 Node.js/Python 的多协程场景都需考虑"谁有权清理全局状态"。

    启示三:函数式更新不是全有或全无

    {…state, …updates} 在顶层保持不可变语义,但 messages.push() 在热点路径上允许可变操作。务实的选择比教条的纯粹更有价值。性能关键路径上的可变操作,只要边界清晰(只在特定位置允许),就是合理的工程取舍。

    启示四:DI 框架不是唯一答案

    4 个 mock 点不需要 InversifyJS。手动定义 QueryDeps + Partial<QueryDeps> 就够了。当依赖注入点 < 10 个时,手动 DI 比框架 DI 更清晰。typeof 技巧确保类型同步,无需额外维护。


    下期预告

    第05篇:30+ 工具背后的秘密——工具系统注册、调度与沙箱安全

    QueryEngine 是 Agent 的大脑,工具系统就是它的"手脚"。一个没有工具的 LLM 只能说话,拥有工具的 Agent 才能行动。下一篇走进第4章,看 Claude Code 如何在赋能与防御之间找到精确的平衡点——30+ 工具的注册调度、BashTool 的七层安全防御、FileEditTool 的原子性写入。


    思考题:Session Memory 压缩依赖后台 Agent 持续维护笔记。如果后台 Agent 出现延迟,笔记不够"新鲜"——系统应该降级到 Full Compact,还是使用"过时的笔记 + 近期消息"的折中方案?


    📖 本系列基于《Claude Code 架构解密》精读整理,系列共20篇,本文为第04篇。 上一篇:第03篇 死循环里的优雅:QueryEngine 的 while(true) 状态机与原子操作 下一篇:第05篇 30+ 工具背后的秘密:工具系统注册、调度与沙箱安全(待发布)

    赞(0)
    未经允许不得转载:171主机测评 » 流式、幂等、安全:QueryEngine 的工程保障三件套
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址