Skip to content

流式响应同包包含 content 与 tool_calls 时丢失工具调用(真实请求复现) #364

Open

Description

@cloviswang

2026-10-29 补充:正式发布版本均真实复现,最新版尚未解决

本轮从 PyPI 下载并校验正式发布 wheel,分别隔离加载 trpc-agent-py 1.1.4、1.1.20、1.1.21、1.2.0。查询时 PyPI 最新版为 1.2.0。每个版本都核对了实际导入路径、版本和解析文件 SHA256。

先运行真实网络请求,再核对源码。所有版本均使用发布包原始 _generate_stream 和原始网络客户端;没有替换解析方法,没有构造、重排或回放响应包。观测方法只读字段并调用原方法。使用相同的无副作用工具声明和下文中文提示,每个版本发起 10 次真实请求。

正式发布版本 真实请求数 出现混合包的请求数 工具调用丢失数 请求异常数
1.1.4 10 5 5 0
1.1.20 10 3 3 0
1.1.21 10 3 3 0
1.2.0(当前最新) 10 3 3 0

14 次丢失均具有相同证据链:服务返回非空 content 与工具名、调用 ID 同包;整轮参数能解析为合法 JSON;SDK 累积的参数完整,但工具名与 ID 为空;最终没有 function_call,error_code 为空,服务结束原因为 tool_calls。其余 26 次未出现混合包,原始调用与解析输出一致。

实测后检查四个发布包源码,仍存在“仅无文本包才调用工具累积函数”的互斥分支。最新版 1.2.0 的 _openai_model.py 第 2523 行仍是该判断。因此保留此 Issue:问题不只存在于 1.1.4,1.1.20 和最新 1.2.0 也尚未解决。

这些是定向提示的小样本计数,不是生产发生率;不能据此比较各版本发生概率。实验连接 OpenAI-compatible 网关,未验证供应商直连接口,也不证明历史业务故障的原因。没有执行业务工具。

以下保留早先 1.1.4 的详细记录。


问题

在 trpc-agent-py 1.1.4 的真实流式请求中,观察到模型服务通过 OpenAI-compatible 接口返回的同一个包同时包含非空 content 和 tool_calls。SDK 丢失了该包中的工具名与调用 ID,最终没有输出工具调用,也没有返回错误。

修复 PR #363 已重新打开并转为 Ready for review,已关联本 Issue。Issue 保持打开,等待修复评审与合入。

环境与验证方式

  • Linux,Python 3.11,trpc-agent-py 1.1.4。
  • 实际运行 trpc_agent_sdk.models.OpenAIModel._generate_stream。
  • 使用 SDK 的真实 OpenAI 网络客户端连接一个 OpenAI-compatible 模型网关。
  • 观测代码只读取进入解析器的包结构,以及工具调用累积后的字段;原版实验不替换客户端、响应包或解析方法。
  • 未执行业务工具。工具仅为无副作用的 record_result,参数为整数 count 和字符串 label。
  • 不公开内部网关地址、认证信息或隐藏推理正文。未验证供应商直连接口是否也返回相同分包。

触发请求

给模型提供 record_result(count: integer, label: string) 工具,发送:

先用一句话说明你会记录结果,然后调用 record_result,count=12,label="中文参数验证"。不要用正文代替工具调用。

采用 Chat Completions 流式请求,开启 stream_options.include_usage。是否触发取决于服务实际分包,不能保证每次重现。

一次真实请求的观测结果

下表为捕获的字段摘要,不是人工构造的响应包。

观测位置 实际结果
第 4 个包 content 长度为 3 字节;同包 tool_calls[0] 包含 index=0、非空调用 ID、name=record_result;arguments 为空字符串
整轮原始工具片段拼接 工具名和 ID 完整;参数共 44 字节,能解析为 JSON 对象
SDK 完成累积时 工具名为空,ID 为空;参数共 44 字节,其 SHA256 与原始参数一致
服务结束原因 tool_calls
SDK 最终结果 没有 function_call,error_code=None;收到最终响应,未抛异常

参数本身没有丢失;丢失的是混合包中的工具名和 ID,导致最终调用无法生成。

重复验证与范围

在探索及确认阶段,原版解析器的 15 次同一模型路由请求中,7 次观察到混合包;这 7 次均出现原始工具调用与 SDK 输出不一致。另一模型路由的 9 次原版请求没有观察到混合包,也没有调用丢失。不同请求条件并不完全相同,这些计数不是线上发生率。

源码中 _generate_stream 只有在 _verify_text_content_in_delta_response 返回 false 时才调用工具累积逻辑,与这次观测吻合。请维护者确认:是否应该支持同一 delta 同时带文本与工具调用?如果该输入受支持,当前分支会遗漏调用信息。

本 issue 不把这一现象归因于模型本身,也不声称它解释了其他应用中的历史故障。

Activity

  1. cloviswang commented on Oct 29, 2026

    @cloviswang
    Author

    补充提交历史分析,以及当前 main 的真实请求验证。

    历史来源

    • 首个公开提交 f485ee4712d5caba27f849304b95e9014f453fdf(2026-04-01,初始化项目)已经包含该分支:只有没有文本的 chunk 才调用 _process_chunk_without_content。注释明确把无文本 chunk 当作工具调用出现的位置。公开历史不能继续追溯到这个决定的最初设计过程,初始化提交也没有解释依据。
    • bugifx: 修复tool文本和正常文本同时存在的时候文本丢失的边界问题 #93 / ac5581d 修复的是文本模拟工具调用时,正常正文被过滤的问题。它处理 add_tools_to_prompt 的文本过滤,不是原生 delta.tool_calls 混合包。此次修改保留了上述分支。
    • Bugfix: 修复LLM返回工具参数非法JSON导致Agent丢弃此工具调用的问题 #257 / 25757d6 修复非法 JSON 参数导致调用被丢弃的问题。它对已经累积到的参数做修复或保留错误信息,不能恢复在前面分支中漏掉的工具名。此次修改同样保留了上述分支。
    • 重试与 HTTP client 管理改造移动过该代码,但没有改变文本与工具调用的互斥判断。

    因此,这个行为在公开仓库初始化时已经存在,不是上述近期修复引入的回归。目前没有找到提交说明宣称“混合 delta 不受支持”,也不能推断作者最初为什么作出该假设。

    当前 main 的真实请求复现

    补测基线为 594bfb099358109c16cda77b86a5a020e262c04e。直接导入该基线对应 SDK 源码;原版组运行其原始 _generate_stream 方法,修复组运行此前补丁中的方法。所有请求均使用 SDK 创建的真实网络客户端,没有替换客户端或构造返回包。

    解析器 真实请求数 收到混合包的请求数 调用丢失数
    main 原版 6 1 1
    最小修复 6 2 0

    原版的失败请求中,工具名与 ID 所在片段与非空 content 同包。服务端参数能完整解析为 JSON,但 SDK 最终没有工具调用。修复组实际遇到的两次混合包均保留调用及完整参数。其他请求没有混合包,调用均一致。请求没有异常。这是独立网络请求的逐次核对,不是同一响应回放,也不代表生产发生率。

    结合已安装 1.1.4 的真实复现,我认为可以确认此 OpenAI-compatible 路径存在静默丢失调用的问题,并且当前 main 仍受影响。原始参数合法,所以本例不属于 #257 的非法 JSON 场景。混合包由底层服务还是中间网关产生,仍未确定。

    建议让工具字段累积独立于文本处理;另可考虑检查 finish_reason=tool_calls 与最终调用列表为空的矛盾,避免静默结束。后者应单独评估错误处理契约。本评论补充问题证据,保持此前已关闭的 #363 不变。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions