Files
zyt/artifacts/qwen_no_answer_review_20261026.md
2026-10-27 14:03:35 +08:00

12 KiB
Raw Permalink Blame History

千问经 Dify 无正文:只读审查

日期:2026-10-26。用户已明确“保留千问,继续排查”。本审查不建议切换模型。

结论

当前优先排查 Dify 被 API key 选中的已发布应用、LLM provider 的实际请求参数/路径,以及 provider 是否缓冲正文。不能从“模型服务在生成 token”或“没有 first_answer_ms”单独确认 thinking 已开启,也不能确认本地输出过滤吞掉了答案。

已保存的 v39 UI 参数只能确认当时温度开关开启且为 0.2;最大标记开关关闭,显示的 512 不代表实际生效上限;推理格式开关也关闭,不能将此解释成模型 thinking 被关闭。快照中没有明确的 enable_thinking / chat_template_kwargs 设置。

本地过滤确实会隐去未闭合 <think>...,但普通正文会立即穿过过滤器,没有“等整篇报告完成才放出正文”的行为。是否由过滤造成无正文,可以通过已有 message 事件计数与安全的数值过滤计数区分,不需要记录推理原文。

范围与已有对照证据

仅只读检查以下代码与公开保存的 UI 快照,并运行纯离线合成测试:

  • server/app/common/service/DifyChatService.php
  • server/app/common/service/DifyReasoningTextFilter.php
  • server/app/common/service/DifyStreamTiming.php
  • server/tests/DifyReasoningFilterTest.php
  • artifacts/dify_qwen_visible_applied_v39.json
  • artifacts/dify_qwen_visible_applied_v39_final.json
  • artifacts/dify_qwen_parameters_restored_v39.txt

本审查没有读取凭据、.env、真实病历或运行日志,没有访问网络,没有修改应用/配置/测试;本报告为此次唯一新建文件。

主代理提供的脱敏运行证据(本审查未重新采集):

  1. 当前 5 张实际附件均能通过当前下载器的 TLS 读取。
  2. 15:22 请求的 Dify LLM node 约 951ms 启动,约 83 秒无正文,应用约 93 秒返回 UPSTREAM_TIMEOUT。
  3. 无患者数据的 15 秒合成 Dify 短请求也没有正文,约 16.8 秒超时并停止 workflow。
  4. 千问服务 metrics 当时为 8 running / 0 waiting,10 秒生成 token 总数由 9804787 增至 9809163。这个总量覆盖服务上的并发请求,不能归因于某一条 Dify 请求,也不能区分正文与 reasoning。
  5. 同期直接请求内部 qwen3.6-35b,使用纯合成 Connectivity check、max_tokens=24、chat_template_kwargs.enable_thinking=false:约 161ms 首 content、222ms 完整结束、13 字节正文、0 reasoning。

第 5 条证明这个直接请求路径上的千问本体能迅速生成正文,排除了“千问服务整体完全不工作”的解释。由于直接请求明确禁用了 thinking,而 Dify 的有效参数尚未证实,两条请求仍不能视为参数完全相同的实验。

1. 应用请求不会替 Dify 选择实际 LLM 参数

DifyChatService::buildRequestSpecs()(1313–1381 行)构造 Dify payload 时只发送 inputs/query/response_mode/user/auto_generate_name/files。没有发送 model、temperature、max_tokens、enable_thinking 或 chat_template_kwargs。

model 参数只在同一函数的另一种协议 payload 中使用(1354–1363 行)。这里描述的是协议边界,不建议改走另一条模型路线。

因此:服务端配置将 profile 命名为 qwen 或把模型名写成 qwen3.6-35b,不会覆盖被 Dify 应用 API key 选中的已发布 workflow 节点。实际模型、provider、endpoint、推理配置、默认输出上限与 provider 是否流式,由该已发布应用和 provider 设置决定。

这是当前最值得验证的边界:不能只核对编辑器中的草稿 UI 或本地配置模型名,需要核对当前请求使用的已发布应用有效节点。

2. v39 快照能证明什么

dify_qwen_parameters_restored_v39.txt:259–309 保存的是 UI 状态,显示模型 qwen3.6-35b CHAT。参数相邻开关及字段为:

UI 项目 保存状态 可以得出的结论
温度 switch=1;数值约 0.2 当时该参数开启
Top P switch=0;显示 1 不能断言传给 provider 的实际值就是 1
频率惩罚 switch=0;显示 0 不能将显示值当生效值
存在惩罚 switch=0;显示 0 同上
最大标记 switch=0;slider 显示 501,stepper 显示 512 512 并非已启用上限,实际默认值未保存
回复格式 switch=0;未选择 没有证据表明该 UI 开启了格式约束
推理格式 switch=0;未选择 没有明确的模型 thinking 开关证据
JSON Schema switch=0 没有证据表明该 UI 强制 schema
停止序列 输入框未显示内容 快照没有证实自定义 stop 序列

visible_applied_v39*.json 只有 switches/system/userHtml 三个顶层字段;switches 是无标签的 7 项布尔字符串,不能独立对应或推断某个隐藏参数。两份 JSON 都没有 enable_thinking、chat_template_kwargs、reasoning_format、max_tokens 或 thinking_budget 配置键。

系统提示词明确要求“直接交付正文”“不得输出思考、流程、等待安排”,并要求遵循服务端任务。它没有请求输出 chain-of-thought,但自然语言要求不显示思考,不等于从推理引擎层禁用 thinking。系统提示仍包含较多临床格式约束,所以一条短 user query 经过 Dify 后也未必等于直接服务的最短 prompt。

同一 txt 的 229–235 行可见“直接回复:LLM / text”的 UI 连线。这个历史草稿画面降低了“当时完全没接 answer node”的可能性,但不证明当前发布图、变量绑定或 provider 流输出完全正确。

3. 本地 reasoning 过滤可以造成零正文,但可以区分

DifyReasoningTextFilter 的行为:

  • 仅识别大小写不敏感的精确 <think> / </think>,支持跨 chunk 和嵌套。
  • 标签内部字节被丢弃;未闭合 think 块直到流结束仍不会转成临床正文。
  • 普通正文在 depth=0 时立即返回,只会暂缓不完整标签前缀和开头的纯空白。
  • reasoning_content、thought 等专用推理字段不是临床正文,解析器不会把它们交给临床 delta。
  • 阻塞与流式回复使用同一类过滤;完整终态只有 reasoning 而无正文,会得到 EMPTY_RESPONSE,不是成功报告。

DifyChatService.php:2597–2601 只把 Dify message/agent_message 的 answer 字符串送给过滤器。node_started/node_finished、agent_thought 等事件不会被当成临床正文。若实际发布应用输出另一套协议/事件形状,应在 Dify/节点绑定处核对,不能用解除过滤来掩盖协议不匹配。

关键计时盲区

recordStreamFrame()(2653–2662 行)在交给 DifyStreamTiming 前,已将 answer 替换成过滤后的可见文字。所以:

  • first_answer_ms 缺失只说明没有可见正文,不证明没有上游 answer 帧。
  • 即使 answer 全被过滤,events.message.count / events.agent_message.count 仍会累计。
  • 原始 <think> 内联内容不会自动进入 reasoning_frame_count;该字段只数 Dify thought 或专用 reasoning_content 字段。reasoning_frame_count=0 也不能单独排除内联 thinking。
  • 过滤器已有安全的 counts():filtered_bytes/think_tag_count/unclosed_think_depth,但当前 DifyChatService 没有将这些计数接入返回/上游 timing,因此现有生产 timing 可能无法区分这两种零正文。

实际执行的纯离线对照

用 Reflection 调用生产 consumeStreamFrame(),为每个用例创建真实 request-local filter 与 timing,仅汇总数字:

合成输入 可见字节 message 帧数 first_answer reasoning_frame_count filter 计数
message.answer="synthetic-body" 14 1 有 0 全 0
message.answer="<think>synthetic-reasoning" 0 1 无 0 filtered_bytes=26,tag=1,depth=1
只有 agent_thought.thought 0 0 无 1 全 0
只有 node_started 0 0 无 0 全 0

这证明本地过滤没有普遍抑制普通正文,同时证明“0 首正文 + 0 专用 reasoning 计数”仍可能是内联 think。若某次原始 Dify 事件里完全没有 message/agent_message,本地过滤器没有输入可吞,排查应前移至 Dify/provider。

完整现有离线回归通过:

php server/tests/DifyReasoningFilterTest.php
Dify incremental reasoning filter: 3660 assertions passed (pure offline fixtures).

4. 最小验证建议,保留千问

主代理正在执行有限的直接服务探测,本审查不重复发请求。建议按下面顺序收敛,每步只记录固定枚举、布尔值、字节数、帧数和耗时,禁止保存临床内容或推理原文。

  1. 核对实际发布配置而非草稿。 对当前 API 应用解析出已发布 LLM 的模型名/provider 标识、endpoint 是否与已验证直接路径一致、provider stream 开关、thinking 开关/extra body、实际 max_tokens、reasoning_format、stop/response_format、重试与 timeout。仅提取非秘密白名单字段,不显示 token、鉴权 header、连接 URL 中的凭据或整份 provider 配置。
  2. 与已成功直接对照使用同一组有效参数。 对无患者数据的最短输出任务,比较同一千问 provider 在 enable_thinking=false 且短输出预算下,经 Dify 时是否同样快速得到 content。单独把 max_tokens 降到 24 而保留 thinking 不明,可能只截断推理而仍无正文,不是干净对照。不要据此直接修改患者应用的默认输出上限。
  3. 区分 provider 原始输出与 Dify 输出。 对合成请求只观察:first byte、first reasoning、first content、usage/finish_reason,以及 Dify message/agent_message 帧数量。直接 provider 快而 Dify provider 慢,优先核对发布应用连接路径/参数/流式适配;provider 有 content 而 Dify 没有 message,则核对 answer 节点、text 变量绑定或 provider/plugin 缓冲。
  4. 若 Dify 有 message、应用没有正文,再看过滤计数。 可在纯合成诊断路径读取 request-local filter 数字;无需关闭过滤,更不能把 reasoning 透传到患者 UI。unclosed_think_depth>0 可直接支持“正在/一直处于内联 think 块”;普通 answer 字节大于 0、过滤字节为 0 却没有 delta,则另查已取消/timeout discard 或 callback_error。

如果需要临时隔离 workflow 复杂度,使用合成诊断路径/临时独立测试应用,只保留同一千问 provider 和一进一出的最短文本任务,先读实际发布配置再做单变量对照。此为验证建议,本审查没有创建应用、发布版本或更改任何参数。

可排除与不能排除

  • 当前直接千问对照已证明该路径可快速返回正文;生成服务整体挂死的解释不成立。
  • 附件可读且无附件合成 Dify 请求也无正文,使“仅由真实附件下载失败导致此次零正文”的解释不成立。
  • 本地普通正文通过 3660 项过滤回归及直接合成帧验证;没有证据表明过滤器会无条件压住正常正文。
  • 尚不能排除 Dify 有效 thinking 默认值、过大的有效输出/推理预算、provider/plugin 只输出或缓冲 reasoning、实际发布应用/endpoint 差异、answer 节点绑定或流式适配问题。
  • UPSTREAM_TIMEOUT 是原 deadline 到达后的终态;单纯已完成的纯 reasoning 结果应为 EMPTY_RESPONSE。当前现象更符合截止前没有可见正文完成,但没有原始帧摘要时仍不能确定具体是哪一层。

本审查没有发现应先改本地过滤器或切换模型的依据。优先把已成功的“同一千问、短预算、明确禁用 thinking”直接对照与 Dify 实际发布请求拉齐,再定位输出消失在哪个边界。