游乐游手机版
首页/AI教程/文章详情

标题长度硬性要求:只输出一个,30汉字60字符内无多余内容

时间:2026-06-04 17:03
normalizeMessagesForAPI 逐行深度解析:Claude Code 消息转换核心机制 如果你研究过 Claude Code 的源码,一定知道消息转换是整个对话系统的中枢神经。这次我们逐行剖析的 normalizeMessagesForAPI 函数,位于 src utils mess

normalizeMessagesForAPI 逐行深度解析:Claude Code 消息转换核心机制

如果你研究过 Claude Code 的源码,一定知道消息转换是整个对话系统的中枢神经。这次我们逐行剖析的 normalizeMessagesForAPI 函数,位于 src/utils/messages.ts:1989-2370。它的核心任务看似简单——将内部的 8 种消息类型压缩成 API 能够接受的 user + assistant 两种格式——但其中暗藏的设计巧思与边界处理逻辑,非常值得深入研究。

query.ts 中 message的处理流程

用户输入 (REPL.tsx)→ onQuery (2855)→ onQueryImpl (2661)→ query() (query.ts:219)→ queryLoop() (query.ts:241)→ deps.callModel (query.ts:659 → deps.ts:35 →queryModelWithStreaming)→ queryModel() (claude.ts:1017)→ normalizeModelStringForAPI(options.model) (claude.ts:1700)

一、函数签名:清晰定义输入输出

export function normalizeMessagesForAPI(
  messages: Message[],        // 完整的内部消息数组
  tools: Tools = [],         // 可用工具列表(用于过滤不存在的 tool_reference)
): (UserMessage | AssistantMessage)[]   // 返回只包含 user/assistant 的消息数组

功能一目了然:将内部的 Message[](涵盖 8 种类型)转换为 API 请求所要求的格式(仅保留 user + assistant),并在此过程中完成过滤、合并与标准化等所有操作。

二、逐行深入分析

第 1993-1994 行:构建可用工具名称集合

const a vailableToolNames = new Set(tools.map(t => t.name))

这一步骤为后续过滤不可用工具引用做准备。当工具搜索功能启用时,需要移除那些已经断开的 MCP 服务器对应的 tool_reference 块。

第 1996-2001 行:重排附件并移除虚拟消息

const reorderedMessages = reorderAttachmentsForAPI(messages).filter(
  m => !((m.type === 'user' || m.type === 'assistant') && m.isVirtual),
)

两步关键操作:

  1. reorderAttachmentsForAPI(messages):将附件消息(attachment)向上浮动,直到遇到 tool_resultassistant 消息。确保附件出现在正确的位置,不打断 tool_use/tool_result 的配对关系。
  2. 过滤 isVirtual:移除标记为 virtual 的 user 和 assistant 消息。这些消息仅用于 UI 展示(例如 REPL 内部工具调用),绝对不能发送到 API。

第 2003-2010 行:错误类型到内容块类型的映射表

const errorToBlockTypes: Record> = {
  [getPdfTooLargeErrorMessage()]:   new Set(['document']),
  [getPdfPasswordProtectedErrorMessage()]: new Set(['document']),
  [getPdfInvalidErrorMessage()]:    new Set(['document']),
  [getImageTooLargeErrorMessage()]:  new Set(['image']),
  [getRequestTooLargeErrorMessage()]: new Set(['document', 'image']),
}

定义每种 API 错误对应的内容块类型。当 API 返回 PDF 过大、加密、无效、图片过大或请求过大等错误时,后续请求必须移除对应的 documentimage 块,否则会反复触发相同的错误。

第 2012-2054 行:构建内容剥离映射表

const stripTargets = new Map>()

Map>:记录"哪条用户消息需要剥离哪些类型的 content block"。

内层循环详细解析(第 2015-2054 行)
遍历所有重排后的消息:
│
└─ 跳过非合成错误消息 (isSyntheticApiErrorMessage=false)
│
└─ 从合成错误消息中提取错误文本
│
└─ 在 errorToBlockTypes 中查找对应的 block 类型
│
└─ 向前回溯,找到最近的前一条 isMeta 用户消息
│
├─ 找到 → 将 block 类型加入 stripTargets[用户消息UUID]
└─ 遇到 assistant/非 meta 用户 → 停止回溯

目的是找到"哪条用户消息附带的 PDF 或图片导致了 API 错误",之后从那条消息中移除对应的 content block。

第 2056-2075 行:过滤不适合进入 API 的消息类型

const result: (UserMessage | AssistantMessage)[] = []
reorderedMessages.filter((): _ is UserMessage | AssistantMessage | ... => {
  if (_.type === 'progress' ||
      (_.type === 'system' && !isSystemLocalCommandMessage(_)) ||
      isSyntheticApiErrorMessage(_)) {
    return false   // 过滤掉
  }
  return true   // 保留
}).forEach(message => { ... })

过滤规则一览:

消息类型是否保留原因
progress❌ 丢弃仅用于 UI 进度展示
system(非 local_command)❌ 丢弃系统消息只在 UI 侧存在
system(local_command)✅ 保留其他 system 消息(compact_boundary、api_metrics、turn_duration 等)都只在 UI侧展示,不发给模型。但 local_command 记录了用户执行了什么命令、看到了什么输出,模型需要这些内容才能理解上下文,因此它被转为 user 消息发给 API。
synthetic API error❌ 丢弃上一步已用其内容构建了 stripTargets
user / assistant / attachment✅ 保留核心对话内容

接下来的 .forEach() 对每种保留的消息类型进行差异化处理。

第 2078-2092 行:system(local_command)→ 转换为 user 消息

case 'system': {
  const userMsg = createUserMessage({
    content: message.content,
    uuid: message.uuid,
    timestamp: message.timestamp,
  })
  const lastMessage = last(result)
  if (lastMessage?.type === 'user') {
    result[result.length - 1] = mergeUserMessages(lastMessage, userMsg)
    return
  }
  result.push(userMsg)
  return
}

local_command 类型的系统消息存储的是本地命令的输出内容(如 /help/compact 等),需要转为 user 消息后发给模型。如果上一条也是 user 消息,则合并到一起(Bedrock 不支持连续 user 消息)。

第 2094-2199 行:user 消息处理全流程

这是 user 消息处理的完整流程,包含 5 个子步骤:

步骤 1(第 2103-2111 行):剥离 tool_reference 块
let normalizedMessage = message
if (!isToolSearchEnabledOptimistic()) {
  normalizedMessage = stripToolReferenceBlocksFromUserMessage(message)
} else {
  normalizedMessage = stripUna vailableToolReferencesFromUserMessage(
    message,
    a vailableToolNames,
  )
}
  • 工具搜索未启用:移除所有 tool_reference 块(该功能需要 beta 标头)
  • 工具搜索已启用:仅移除已断开连接的 MCP 工具的引用
步骤 2(第 2116-2137 行):剥离错误内容块
const typesToStrip = stripTargets.get(normalizedMessage.uuid)
if (typesToStrip && normalizedMessage.isMeta) {
  const content = normalizedMessage.message.content
  if (Array.isArray(content)) {
    const filtered = content.filter(block => !typesToStrip.has(block.type))
    if (filtered.length === 0) { return }   // 全部被剥离 → 完全跳过这条消息
    if (filtered.length < content.length) {
      normalizedMessage = {
        ...normalizedMessage,
        message: { ...message.message, content: filtered }
      }
    }
  }
}

利用第 2012-2054 步构建的 stripTargets 映射表。如果某条 meta 用户消息的 PDF/图片曾导致 API 错误,则从后续请求中移除对应的 document/image 块。如果所有块都被移除了,整条消息直接跳过。

步骤 3(第 2159-2185 行):注入 tool_reference 分界文本
if (!checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_toolref_defer_j8m')) {
  const contentAfterStrip = normalizedMessage.message.content
  if (
    Array.isArray(contentAfterStrip) &&
    !contentAfterStrip.some(b => b.type === 'text' && b.text.startsWith(TOOL_REFERENCE_TURN_BOUNDARY)) &&
    contentHasToolReference(contentAfterStrip)
  ) {
    normalizedMessage = {
      ...normalizedMessage,
      message: {
        ...normalizedMessage.message,
        content: [
          ...contentAfterStrip,
          { type: 'text', text: TOOL_REFERENCE_TURN_BOUNDARY },
        ],
      },
    }
  }
}

当消息中包含 tool_reference 块时,服务端会将其渲染为 ...。如果这些内容位于 prompt 尾部,某些模型约有 10% 的概率会错误采样到 stop sequence。解决方案:在 tool_reference 块后追加一个 nnHuman: 文本块作为明确的轮次边界。

tengu_toolref_defer_j8m 门控:该标志启用时,由后处理 relocateToolReferenceSiblings() 接管,此处跳过注入。

幂等性保障:query.ts 每轮都会调用此函数,已追加的文本块通过 startsWith(TOOL_REFERENCE_TURN_BOUNDARY) 检测并跳过。

步骤 4(第 2187-2195 行):合并连续 user 消息
const lastMessage = last(result)
if (lastMessage?.type === 'user') {
  result[result.length - 1] = mergeUserMessages(lastMessage, normalizedMessage)
  return
}

与上一条 user 消息合并。Bedrock 不支持连续 user 消息(1P API 虽然支持也会自动合并,但显式合并更可靠)。

步骤 5(第 2198 行):追加到结果数组
result.push(normalizedMessage)

如无合并场景,直接追加到结果数组。

第 2201-2267 行:assistant 消息处理

工具输入标准化(第 2206-2244 行)
const toolSearchEnabled = isToolSearchEnabledOptimistic()
const normalizedMessage: AssistantMessage = {
  ...message,
  message: {
    ...message.message,
    content: message.message.content.map(block => {
      if (block.type === 'tool_use') {
        const tool = tools.find(t => toolMatchesName(t, block.name))
        const normalizedInput = tool
          ? normalizeToolInputForAPI(tool, block.input as Record)
          : block.input
        const canonicalName = tool?.name ?? block.name

        if (toolSearchEnabled) {
          return { ...block, name: canonicalName, input: normalizedInput }
        }

        // 工具搜索未启用:只保留标准字段,strip 掉 caller 等额外字段
        return {
          type: 'tool_use' as const,
          id: block.id,
          name: canonicalName,
          input: normalizedInput,
        }
      }
      return block   // text/thinking 等非 tool_use block 透传
    }),
  },
}

tool_use block 执行三项关键操作:

  1. 工具名规范化:通过 toolMatchesName 匹配已注册的工具名(处理别名)
  2. 输入标准化:normalizeToolInputForAPI 移除工具内部字段(例如 ExitPlanModeV2plan 字段)
  3. 工具搜索字段剥离:未启用工具搜索时,显式构造只有 type/id/name/input 四个字段的 block,确保不会漏出 caller 等工具搜索专用字段(这些字段存在于对话记录中,若没有工具搜索 beta 标头发送会导致 API 400 错误)
合并相同 ID 的 assistant 消息(第 2246-2264 行)
for (let i = result.length - 1; i >= 0; i--) {
  const msg = result[i]!
  if (msg.type !== 'assistant' && !isToolResultMessage(msg)) {
    break   // 遇到非 assistant 且非 tool_result 就停止
  }
  if (msg.type === 'assistant') {
    if (msg.message.id === normalizedMessage.message.id) {
      result[i] = mergeAssistantMessages(msg, normalizedMessage)
      return   // 合并后直接返回,不追加
    }
    continue
  }
}
result.push(normalizedMessage)

当 API 响应包含多个 content block 时,normalizeMessages()(早期处理)会将它们拆分为多条独立的 AssistantMessage。这里反过来:找到同一条 API 响应的不同 block,合并回同一条消息。

关键设计:只合并 message.id 相同的 assistant 消息。因为并发 agent(队友)可能交错插入来自不同 API 响应的流式内容 block,它们的 message.id 不同,不应合并。搜索范围限制在最近的连续的 assistant/tool_result 区间,遇到 user 消息就停止。

第 2269-2291 行:attachment 消息处理

case 'attachment': {
  const rawAttachmentMessage = normalizeAttachmentForAPI(message.attachment)
  const attachmentMessage = checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_chair_sermon')
    ? rawAttachmentMessage.map(ensureSystemReminderWrap)
    : rawAttachmentMessage

  const lastMessage = last(result)
  if (lastMessage?.type === 'user') {
    result[result.length - 1] = attachmentMessage.reduce(
      (p, c) => mergeUserMessagesAndToolResults(p, c),
      lastMessage,
    )
    return
  }
  result.push(...attachmentMessage)
  return
}

normalizeAttachmentForAPI() 将内部的 Attachment 类型转换为 API 可用的 UserMessage[](一个附件可能展开为多条 user 消息)。如果 tengu_chair_sermon 门控开启,对每条消息调用 ensureSystemReminderWrap(将 文本块与相邻的 tool_result 合并)。

如果上一条是 user 消息,则合并到一起;否则追加到结果数组。

第 2295-2305 行:重定位 tool_reference 文本块

const relocated = checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_toolref_defer_j8m')
  ? relocateToolReferenceSiblings(result)
  : result

tengu_toolref_defer_j8m 门控开启时,将第 2159-2185 步注入的轮次边界文本从 tool_reference 所在的 user 消息移动到后一条不包含 tool_reference 的 user 消息上。这样可以避免连续两个 user 消息(一个带 tool_reference,一个带文本)的模式——这种模式会误导模型在 tool result 后发射 stop sequence。

门控关闭时,这是一个空操作(noop),由第 2159-2185 步的注入充当 fallback。

第 2307-2311 行:过滤孤儿 thinking-only 消息

const withFilteredOrphans = filterOrphanedThinkingOnlyMessages(relocated)

过滤掉只有 thinking block 的 assistant 消息。这种消息通常是压缩(compaction)切断了流式响应和重试之间的中间消息导致的。如果不移除,连续两条 assistant 消息的 thinking block 签名不匹配会导致 API 400 错误。

第 2313-2325 行:三重后处理清理

const withFilteredThinking = filterTrailingThinkingFromLastAssistant(withFilteredOrphans)
const withFilteredWhitespace = filterWhitespaceOnlyAssistantMessages(withFilteredThinking)
const withNonEmpty = ensureNonEmptyAssistantContent(withFilteredWhitespace)
  1. filterTrailingThinkingFromLastAssistant:移除最后一条 assistant 消息末尾的 thinking block。最后一条消息的 thinking block 可能是不完整的(被 max_tokens 截断或在消息处理过程中被剥离),其签名无法通过服务端验证。移除后让消息以 text 或 tool_use 结尾。
  2. filterWhitespaceOnlyAssistantMessages:过滤内容只有空白文本的 assistant 消息。顺序很关键:必须先 strip thinking,再过滤空白。否则 [text("nn"), thinking("...")] 会先通过空白检查(有非 text block),再 strip 掉 thinking 后变成 [text("nn")],API 会拒绝。
  3. ensureNonEmptyAssistantContent:确保每条 assistant 消息的 content 不为空。如果经过前两步处理后某条消息的 content 变成了空数组,则插入一个占位文本。

第 2327-2338 行:合并 + 合并 system-reminder 文本块

const smooshed = checkStatsigFeatureGate_CACHED_MAY_BE_STALE('tengu_chair_sermon')
  ? smooshSystemReminderSiblings(mergeAdjacentUserMessages(withNonEmpty))
  : withNonEmpty

mergeAdjacentUserMessages:合并上一步可能产生的相邻 user 消息(filterOrphanedThinkingOnlyMessages 移除消息后会导致 user 消息相邻)。

smooshSystemReminderSiblings:将 前缀文本块合并到相邻的 tool_result 中。这是为了满足特定服务的格式要求(tengu_chair_sermon 门控)。

第 2340-2343 行:清洗错误 tool_result 内容

const sanitized = sanitizeErrorToolResultContent(smooshed)

无条件执行。将有错误的 tool_result 中的 image 等非文本内容移除。如果在 is_error=true 的 tool_result 中残留了图片内容,恢复会话时会永久返回 400 错误。早期 smooshIntoToolResult 的过滤没有覆盖这个情况,因此这里做兜底清洗。

第 2345-2364 行:追加消息 ID 标签

if (feature('HISTORY_SNIP') && process.env.NODE_ENV !== 'test') {
  const { isSnipRuntimeEnabled } = require('../services/compact/snipCompact.js')
  if (isSnipRuntimeEnabled()) {
    for (let i = 0; i < sanitized.length; i++) {
      if (sanitized[i]!.type === 'user') {
        sanitized[i] = appendMessageTagToUserMessage(sanitized[i] as UserMessage)
      }
    }
  }
}

HISTORY_SNIP 功能启用时,在每条 user 消息的末尾追加 [id:uuid] 标签。这使 SnipTool 可以通过消息 ID 精确删除中间消息。测试环境下跳过,因为标签会改变消息内容的 hash,破坏 VCR fixture 查找。 HISTORY_SNIP 功能(ant-only)提供的一个工具。源文件在此代码副本中不存在,但从使用模式可以还原其设计。

第 2367-2369 行:图片尺寸验证并返回

validateImagesForAPI(sanitized)
return sanitized

检查所有 image block 的尺寸是否在 API 限制内。超限的图片会抛出错误,由调用方处理。

三、处理流程总图:清晰把握整体脉络

输入: Message[](8 种类型)
│
├─ ❶ reorderAttachmentsForAPI() → 附件上浮
├─ 过滤 isVirtual
│
├─ ❷ 构建 stripTargets(错误内容剥离映射表)
│
├─ ❸ 过滤不可 API 的消息类型
│   ├─ progress → 丢弃
│   ├─ system(非 local_command)→ 丢弃
│   ├─ 合成 API 错误 → 丢弃
│   └─ 其余保留
│
├─ ❹ 遍历保留的消息,按 type 分支处理:
│   ├─ system(local_command) → 转为 user,合并相邻 user
│   ├─ user →
│   │   ├─ strip tool_reference(未启用搜索时全部移除)
│   │   ├─ strip 错误内容块(PDF/图片超限)
│   │   ├─ 注入 tool_reference 分界文本
│   │   └─ 合并相邻 user
│   ├─ assistant →
│   │   ├─ 标准化 tool_use 输入
│   │   ├─ strip caller 等工具搜索字段
│   │   └─ 合并相同 API 响应的拆分 block
│   └─ attachment →
│       ├─ normalizeAttachmentForAPI()
│       └─ 合并相邻 user
│
├─ ❺ relocateToolReferenceSiblings(门控)
├─ ❻ filterOrphanedThinkingOnlyMessages
├─ ❼ filterTrailingThinkingFromLastAssistant
├─ ❽ filterWhitespaceOnlyAssistantMessages
├─ ❾ ensureNonEmptyAssistantContent
├─ ❿ mergeAdjacentUserMessages + smooshSystemReminderSiblings(门控)
├─ ⓫ sanitizeErrorToolResultContent
├─ ⓬ appendMessageTagToUserMessage(HISTORY_SNIP)
└─ ⓭ validateImagesForAPI
│
输出: (UserMessage | AssistantMessage)[] — 仅 user + assistant 两种类型

四、涉及的辅助函数一览:工具集总览

函数位置作用
reorderAttachmentsForAPImessages.ts附件上浮,不打断 tool_use/tool_result 配对
isSyntheticApiErrorMessagemessages.ts判断是否为 API 错误消息
isSystemLocalCommandMessagemessages.ts判断是否为本地命令系统消息
mergeUserMessagesmessages.ts合并两条 user 消息的 content
mergeAssistantMessagesmessages.ts合并两条 assistant 消息的 content block
stripToolReferenceBlocksFromUserMessagemessages.ts移除所有 tool_reference 块
stripUna vailableToolReferencesFromUserMessagemessages.ts移除已断连工具的 tool_reference
normalizeToolInputForAPImessages.ts标准化工具输入参数
normalizeAttachmentForAPIattachments.ts附件→UserMessage[] 转换
ensureSystemReminderWrapmessages.ts确保 system-reminder 文本包装
relocateToolReferenceSiblingsmessages.ts重定位 tool_reference 文本块
filterOrphanedThinkingOnlyMessagesmessages.ts过滤孤儿 thinking-only 消息
filterTrailingThinkingFromLastAssistantmessages.ts移除最后一条 assistant 的尾部 thinking
filterWhitespaceOnlyAssistantMessagesmessages.ts过滤空白内容消息
ensureNonEmptyAssistantContentmessages.ts确保 content 不为空
mergeAdjacentUserMessagesmessages.ts合并相邻 user 消息
smooshSystemReminderSiblingsmessages.ts合并 system-reminder 文本到 tool_result
sanitizeErrorToolResultContentmessages.ts清洗错误 tool_result 的图片内容
appendMessageTagToUserMessagemessages.ts追加消息 ID 标签
validateImagesForAPImessages.ts验证图片尺寸限制

ensureToolResultPairing 源码分析

文件: src/utils/messages.ts:5133-5460
功能: 确保消息序列中 tool_usetool_result 成对出现,修复不匹配(孤儿 tool_use、孤儿 tool_result、重复 ID、断流等),防止 API 400 错误。

stripExcessMediaItems 源码分析

作用是限制消息数组中的媒体项数量(图片 + 文档),超过 limit 则从最早的媒体开始剥离。

流程:

  1. 计数 — 遍历所有消息,统计 imagedocument 类型的 content block数量(包括嵌套在 tool_result 内部的),得到总媒体数

  2. 计算差值 — toRemove = total - limit,如果未超限则原样返回

  3. 从旧到新剥离 — 再次遍历消息,按顺序从前往后过滤媒体块,每移除一个toRemove--,直到减到 0(即只剥离最早的、多余的媒体,保留最新的)

来源:https://juejin.cn/post/7646632889624805428
上一篇腾讯云CloudQ与AWS Q多云AI运维助手对比评测 下一篇从Swagger到可执行测试基于Skills+RAG的接口用例智能生成实践
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南
AI教程 · 2026-07-07

科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南

Section 01 多肽 VS 蛋白质 VS 重组蛋白 多肽、蛋白质和重组蛋白,本质上是同宗同源的东西——都是氨基酸串起来的生物大分子。三者的核心区别,说到底无非是三个维度:分子大小、折叠形态,以及生产方式。 接下来是一张清晰的对比图,帮你快速建立直觉: ![对比图1](https:

知识图谱与本体语义建模的核心区别解析
AI教程 · 2026-07-07

知识图谱与本体语义建模的核心区别解析

谈到人工智能如何“理解”知识,有两个概念常被放在一起讨论:知识图谱与本体语义建模。不少人以为它们是同一事物,或者认为后者是前者的进化版。实际上,两者的分工完全不同——打个比方,一个是“记事的本子”,另一个是“写本子之前先定好的规矩”。 1 本体语义建模:先绘制一张“通用分类蓝图” 设想一下,你要整

强烈推荐工作搭子WorkBuddy
AI教程 · 2026-07-07

强烈推荐工作搭子WorkBuddy

一次偶然的机会,从朋友那里了解到WorkBuddy这个工具。说实话,在AI产品扎堆的今天,能遇到一个下载即用的助手,确实值得推荐给每一个被日常琐事缠身的人。 安装过程没什么难度,双击安装包默认安装即可。需要留意的是,如果在Windows7上折腾了半天没反应,别慌——这工具在高版本Windows下运行

跨境电商系统自动化测试与CI/CD流水线构建指南
AI教程 · 2026-07-07

跨境电商系统自动化测试与CI/CD流水线构建指南

技术方向:自动化测试与DevOps实践关键词:日本代购、一站式日淘、雅虎代拍系统、煤炉自动代拍 一、测试分层策略详解 不少人刚开始就想直接搞E2E测试,觉得跑通完整流程才够“真实”。然而,测试金字塔这么多年仍不过时,原因很简单——不同层级的测试各有分工,缺少任何一层都会不稳。来看看这张金字塔图: ┌

中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英
AI教程 · 2026-07-07

中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英

天天刷着别人的爆款内容,自己却“有心无力”——这才是2026年绝大多数中小企业运营社交媒体的真实写照。说白了,社交媒体如今早已不是“要不要做”的选择题,而是“怎么做才能真正见效”的生存考验。现实情况是,团队人力就那么几个,预算也紧巴巴,却要同时运营抖音、小红书、知乎、头条、百家号等多个阵地……文案、