bug(core): `merge_dicts` silently coerces differing `bool` values to `int` during streaming chunk aggregation

该问题通常出现在 LangChain Core 流式输出(streaming)场景,当使用 `AIMessageChunk.__add__` 聚合 `additional_kwargs` 或 `response_metadata` 时,如果同一 key 在不同 chunk 中携带不同的布尔值(如 `

快速结论:该问题通常出现在 LangChain Core 流式输出(streaming)场景,当使用 `AIMessageChunk.__add__` 聚合 `additional_kwargs` 或 `response_metadata` 时,如果同一 key 在不同 chunk 中携带不同的布尔值(如 `True` 和 `False`),`merge_dicts` 会静默将类型从 `bool` 转换为 `int`(结果为 `1`)。优先排查你是否在流式聚合时遇到了布尔类型被“求和”的情况,并更新到包含修复的版本。

适用环境:根据 Issue 确认:langchain-core 1.4.6,Python 3.12.13,Linux 操作系统,pydantic 2.13.4。

最快修复方案:暂无确认的独立修复方案;Issue 中已提出补丁(PR #38071/#38081 待合并),核心修复是在 `merge_dicts` 的 `int` 分支之前添加明确的 `bool` 检查,并采用“后写覆盖(last-wins)”语义。可优先尝试升级到包含该修复的最新版 langchain-core。

注意事项:该修复采用“后写覆盖”语义(与 `index`/`created`/`timestamp` 整数字段处理一致),而非直接抛出 `TypeError`;原因是 `merge_dicts` 处于流式聚合热路径,抛错会中断整个流。修复尚未合并到主分支时,生产环境请谨慎使用。

问题场景

在 LangChain 中使用 `AIMessageChunk` 进行流式输出聚合时,`additional_kwargs` 或 `response_metadata` 中同一 key 在不同 chunk 间携带不同的布尔值。例如,某模型供应商流式返回 `refusal` 标志(可能为 `True` 或 `False`),多个 chunk 中该值不一致时,`merge_dicts`(通过 `AIMessageChunk.__add__` 调用)会把布尔值当作整数相加:`True + False = 1`,导致类型从 `bool` 静默变为 `int`。

报错原文

bug(core): `merge_dicts` silently coerces differing `bool` values to `int` during streaming chunk aggregation

# Direct call
merge_dicts({"a": True}, {"a": False})   # -> {'a': 1}     (expected: a bool)

# Via AIMessageChunk.__add__
a = AIMessageChunk(content="", additional_kwargs={"refusal": True})
b = AIMessageChunk(content="", additional_kwargs={"refusal": False})
(a + b).additional_kwargs                 # -> {'refusal': 1}  (type changed bool -> int)

原因分析

可能原因:Python 中 `bool` 是 `int` 的子类,因此 `isinstance(True, int)` 返回 `True`。在 `merge_dicts` 的整数合并分支中:

elif isinstance(merged[right_k], int):
    if right_k in {"index", "created", "timestamp"}:
        merged[right_k] = right_v
    else:
        merged[right_k] += right_v   # True + False -> 1

当两个 chunk 的同一 key 携带不同布尔值时,会进入整数分支执行 `+=`,将 `True` 和 `False` 相加得到整数 `1`。这个过程中没有抛出异常,但值的类型被静默改变。相等的布尔值则不会触发此问题(会被前面的相等判断短路跳过)。该问题的核心矛盾在于:其他不支持的冲突类型(如 `float`、`tuple`)都会“大声”抛出 `TypeError`,唯独 `bool` 既不安全合并也不报错,导致数据静默损坏。

环境排查

  • 确认 langchain-core 版本是否为 1.4.6 或更早(可通过 `python -m langchain_core.sys_info` 查看)。
  • 确认 Python 版本为 3.12.13 或更早是否存在相同行为。
  • 检查流式输出场景是否涉及 `AIMessageChunk` 的 `additional_kwargs` 或 `response_metadata` 聚合。
  • 检查模型供应商是否返回了布尔类型的元数据字段(如 `refusal`、`finished`、`is_valid` 等)。

解决步骤

  1. 确认问题来源:优先在本地直接调用 `merge_dicts({“a”: True}, {“a”: False})`,验证是否输出 `{‘a’: 1}`(此时为 bug 触发)。
  2. 检查 langchain-core 版本:升级到包含修复的最新版本(合并 PR #38071/#38081 后的发布版);若暂未发布,可考虑从源码分支安装。
  3. 若无法升级版本,可考虑绕过方案:在流式聚合前手动清洗 `additional_kwargs` 或 `response_metadata`,确保同一 key 的布尔值一致,或使用 `reset_metadata`/`override` 等方式覆盖而非合并。
  4. 如果自行修改源码,修复方式是在 `merge_dicts` 的 `int` 分支前增加 `bool` 检查,并采用“后写覆盖”语义:
elif isinstance(merged[right_k], bool):
    merged[right_k] = right_v  # last-wins
elif isinstance(merged[right_k], int):
    ...

验证方法

运行下面的回归测试确认问题已解决:

# 1. 直接调用应输出 bool 类型,而非 int
print(merge_dicts({"a": True}, {"a": False}))  # 修复后应输出 {'a': False},类型为 bool

# 2. 等值布尔仍走短路逻辑
print(merge_dicts({"a": True}, {"a": True}))    # 应输出 {'a': True}

# 3. 通过 AIMessageChunk.__add__ 验证类型保持
a = AIMessageChunk(content="", additional_kwargs={"refusal": True})
b = AIMessageChunk(content="", additional_kwargs={"refusal": False})
result = (a + b).additional_kwargs
assert isinstance(result["refusal"], bool), "布尔类型应被保留,而不是被转换为 int"

参考来源

langchain-ai/langchain #38064

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 18306

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注