📎 配套脚本附件正文未内嵌 · 共 17 个 · 点开可在线查看
📚 另需公共库(正文未重复内嵌,点名字直接看):_agent_cfg.py、_bootstrap.py、_config.py、_plugin_util.py
📂 查看全部 761 个脚本附件 →
⚠️ 脚本内含明文凭据(与本页正文一致),请勿在公网环境直接运行。
Dify · 专题四

Agent 问答报错

System message must be at the beginning。步骤未改,代码块已换成完整程序。

Dify_专题四_Agent问答报错_cursor.md

这是基于原文生成的 Cursor 版文档。

原文件未改动;本版只做三件事:统一阅读入口、修正已审出的明显问题、补充更适合快速排障的提示。

Cursor 版修订说明

阅读提示

运行环境:业务脚本均在 207 固定目录 /opt/workbuddy 执行,Python 为 /usr/bin/python3。 Windows 开发机仅保留 _probe_* 调试脚本;同步命令:python scripts_cursor/完整代码约定/00_部署_workbuddy到207.py。


原文主体(已纳入 Cursor 版纠错)

专题四:创建 Agent 后「尝试问答」报错的综合排查与解决

阅读导航(何时打开 · 报错往哪查)

本文是单点修复/排障专题,配合《安装配置手册》使用;下文实战步骤与代码全部保留,此处只做索引。

报错/现象本文案例还可能是
400 System message案例 A + chat.py专题八 future 顺序
Connection / no node案例 B专题五 7 层
tool_calls 解析异常案例 CvLLM parser qwen3_xml
排查顺序§六 落地清单专题十四 变慢

适用场景:模型已经接好、知识库也接好了,但真正在 Agent 里发消息问答时却报错。

本文按"先定位是哪一层的问题 → 怎么抓证据 → 怎么复现 → 怎么修 → 怎么验证"的思路展开,

所有可用的配置与代码都直接附在文中,可照抄执行。


一、先认识:问答期报错有哪几类

问答发生在请求已经离开 Dify、打到模型(vLLM/Qwen 或 Xinference/BGE-M3)之后,所以报错不可能来自"模型没接入",而几乎都来自下面三类:

类别典型报错出在哪一层一句话根因
A. 消息格式被模型拒绝400 System message must be at the beginning模型端(Qwen chat template)拒绝 Dify 出站消息Dify 把多条 system 堆在前面,Qwen3 只接受一条
B. 模型端点不可达Connection error / no available node / 超时网络/容器未起vLLM 容器没起、端口不通、或插件未就绪
C. 工具调用/推理解析异常工具不触发、JSON 解析错、思考块乱入解析器配置--tool-call-parser 选错(如误用 qwen3)

本文主案例讲 A(最典型、最让人困惑),B 和 C 作为补充案例。

A 和 C 的"根因"都和模型架构强相关,单独部署教程见 Dify_Qwen安装配置手册_cursor.md,vLLM 启动参数见 Dify_专题九_其它配置问题解决_cursor.md。


二、主案例 A:400 System message must be at the beginning

2.1 现象

在 Agent 应用里发一条普通问题(例如"描述考勤与请休假管理制度的目的"),页面/API 直接返回 400:

text
BadRequestError: System message must be at the beginning.

只要走 Agent 类应用(带工具/知识检索) 就必现;普通「聊天助手」应用有时不报——因为 Agent 会额外往 system 区塞一堆提示。

2.2 排查思路(不要急着改,先分层)

text
问答报错
  └─ 是不是模型端拒绝?(400 + "System message must be at the beginning")
        ├─ 是 → 看 Dify 实际发给模型的 messages 长什么样(抓出站日志)
        │        └─ 若多条 system / 含空 system → 根因确认:Qwen3 只接受单条 system
        └─ 否  → 看是不是端点不可达(B) 或 工具解析(C)

关键判断:这条 400 的措辞是模型(Qwen3 的 chat template)自己抛的,说明请求已经到达 vLLM,所以:

2.3 抓证据:看 Dify 实际出站的消息

Dify 的 OpenAI 插件在 plugin_daemon 容器内运行,出站 messages 在容器内 /tmp,宿主机的 /tmp 看不到。

先在插件 chat.py 里临时加一段调试日志(验证完要删掉,见 2.6):

文件:.../openai-1.0.3@<hash>/models/llm/chat.py,在 client.chat.completions.create(...) 调用之前插入:

完整程序:/opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_01.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_01.py

python
    # DEBUG: dump messages before sending to vLLM/Qwen
    try:
        with open('/tmp/dify_openai_messages.log', 'a', encoding='utf-8') as _df:
            _df.write('--- model=%s stream=%s ---\n' % (model, stream))
            _df.write(json.dumps(
                [message(item) for item in _ensure_system_first(prompt_messages)],
                ensure_ascii=False, indent=2))
            _df.write('\n\n')
    except Exception as _e:
        pass

重启插件使日志生效:

完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_01.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_01.sh

bash
#!/usr/bin/env bash
set -euo pipefail

cd /dify/dify-main/docker && docker compose restart plugin_daemon
# 清空旧日志
docker exec docker-plugin_daemon-1 sh -c 'rm -f /tmp/dify_openai_messages.log && touch /tmp/dify_openai_messages.log'

再在 Dify 里发一次问题触发报错,然后读日志:

完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_02.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_02.sh

bash
#!/usr/bin/env bash
set -euo pipefail

docker exec docker-plugin_daemon-1 cat /tmp/dify_openai_messages.log

你会看到:出站消息是多条连续的 system,且其中夹着一条空 system,例如:

json
[
  {"role": "system", "content": "你是企业内部知识助手..."},        // 知识库助手提示
  {"role": "system", "content": "You can run commands in an isolated shell workspace..."}, // Agent shell 工具提示
  {"role": "system", "content": ""},                                // ← 一条空 system
  {"role": "system", "content": "Current Agent config manifest..."},// Agent 配置
  {"role": "system", "content": "Environment variables may contain..."}, // 环境变量提示
  {"role": "user",   "content": "描述考勤与请休假管理制度的目的"}
]

顺序本身没问题(都在最前),但 Qwen3 的 chat template 拒绝"多条 system 且含空 system"。

2.4 复现确认根因(直连 vLLM)

不用 Dify,直接用 curl 打 vLLM,把三种情况都试一遍,确认根因:

完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_03.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_03.sh

bash
#!/usr/bin/env bash
set -euo pipefail

BASE=http://116.62.10.183:8000/v1/chat/completions
AUTH='Authorization: Bearer 123456'
H='Content-Type: application/json'

# TEST1:两条 system → 必报错
curl -s -X POST $BASE -H "$H" -H "$AUTH" -d '{
  "model":"qwen3.6-27b",
  "messages":[{"role":"system","content":"A"},{"role":"system","content":"B"},{"role":"user","content":"hi"}]
}'

# TEST2:含空 system → 必报错
curl -s -X POST $BASE -H "$H" -H "$AUTH" -d '{
  "model":"qwen3.6-27b",
  "messages":[{"role":"system","content":"A"},{"role":"system","content":""},{"role":"system","content":"C"},{"role":"user","content":"hi"}]
}'

# TEST3:合并成单条 system → 成功
curl -s -X POST $BASE -H "$H" -H "$AUTH" -d '{
  "model":"qwen3.6-27b",
  "messages":[{"role":"system","content":"A\n\nB\n\nC"},{"role":"user","content":"hi"}]
}'

结果:TEST1、TEST2 都返回 System message must be at the beginning;TEST3 正常。根因锁定:Qwen3(qwen3_5 架构)的 chat template 只允许"一条 system 消息且必须在最前"。

注意:早期补丁只把 system 消息"排序置顶",没"合并",所以没用——多条 system 依然被拒。

2.5 解决:把多条 system 合并成一条

改插件 chat.py 的 _ensure_system_first 函数(该函数必须放在 from __future__ import annotations 这一行之后,否则会触发 SyntaxError: from __future__ imports must occur at the beginning,导致整个 openai 插件加载失败、保存凭据时报 "no available node")。

完整函数(当前生效版本,直接替换原 def _ensure_system_first 即可):

完整程序:/opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_02.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_02.py

python
def _ensure_system_first(messages: list[PromptMessage]) -> list[PromptMessage]:
    """Qwen3/vLLM chat templates accept ONLY one system message, at the start.

    Dify (esp. Agent apps) stacks several system/developer prompts: KB assistant
    prompt + agent tool prompt + an empty system + config manifest... Merging them
    into a single system message (joined by blank lines, empties dropped) keeps it
    at the front; every other message keeps its original relative order.
    """
    system_parts: list[str] = []
    rest: list[PromptMessage] = []
    for m in messages:
        if isinstance(m, (SystemPromptMessage, DeveloperPromptMessage)):
            text = _text_content(m)
            if text.strip():
                system_parts.append(text)
        else:
            rest.append(m)
    if system_parts:
        rest.insert(0, SystemPromptMessage(content="\n\n".join(system_parts)))
    return rest

其中 _text_content 是插件已有的辅助函数(无需新增):

完整程序:/opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_03.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_03.py

python
def _text_content(prompt: PromptMessage) -> str:
    if isinstance(prompt.content, str):
        return prompt.content
    content = prompt.content or []
    if any(item.type != PromptMessageContentType.TEXT for item in content):
        raise InvokeBadRequestError(
            f"{prompt.role.value} messages only support text content"
        )
    return "".join(cast(TextPromptMessageContent, item).data for item in content)

调用处(已在原 generate_chat 里):

完整程序:/opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_04.py

运行/引用:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_内联_04.py

python
    response = client.chat.completions.create(
        model=model,
        messages=cast(Any, [message(item) for item in _ensure_system_first(prompt_messages)]),
        stream=stream  # 其余参数不变
        ...
    )

改完重启插件加载:

完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_04.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_04.sh

bash
#!/usr/bin/env bash
set -euo pipefail

cd /dify/dify-main/docker && docker compose restart plugin_daemon

2.6 验证(通过 Agent API 实测)

Agent 应用必须用 streaming 模式(blocking 模式会被拒绝),用官方 API key 直接打:

完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_05.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_05.sh

bash
#!/usr/bin/env bash
set -euo pipefail

BASE=http://192.168.110.207/v1
APIK=app-v9yYVTQTrUoSFHNqAhufSGmh

curl -s -N -X POST $BASE/chat-messages \
  -H "Authorization: Bearer $APIK" \
  -H "Content-Type: application/json" \
  -d '{
    "inputs": {},
    "query": "描述考勤与请休假管理制度的目的",
    "response_mode": "streaming",
    "user": "debug-workbuddy"
  }'

预期:

验证通过后,把 2.3 加的临时调试日志代码删掉(保持插件干净): 删掉那段 # DEBUG: dump messages... 到 except Exception as _e: pass 为止的块,再 docker compose restart plugin_daemon 一次。


三、补充案例 B:模型端点不可达(Connection error / no available node)

现象

问答时返回 InvokeConnectionError / Connection error / 保存模型凭据时 no available node。

思路

这是"请求没出 Dify 网关"或"出网关后连不上模型"。优先查 vLLM 是否真的在监听:

完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_06.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_06.sh

bash
#!/usr/bin/env bash
set -euo pipefail

# 在 Dify 服务器上
docker ps --filter name=vllm-qwen3.6-27b        # 容器是否 running
docker logs --tail 20 vllm-qwen3.6-27b          # 是否 "Application startup complete"
curl -s http://116.62.10.183:8000/v1/models     # 端口是否通、模型是否列出

常见根因与处理


四、补充案例 C:工具调用 / 推理解析异常

现象

问答时模型"不调用工具"、或返回里混着思考块、或 JSON 解析报错。

思路

这是 vLLM 启动参数里解析器选错,与模型架构强绑定:

见 vllm_qwen_run.sh 当前生效参数:

完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_07.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_07.sh

bash
#!/usr/bin/env bash
set -euo pipefail

docker.m.daocloud.io/vllm/vllm-openai:latest \
  /model \
  --trust-remote-code --dtype bfloat16 --tensor-parallel-size 1 \
  --enable-auto-tool-choice --tool-call-parser qwen3_xml --reasoning-parser qwen3 \
  --max-model-len 65536 --gpu-memory-utilization 0.75 --max-num-seqs 8 \
  --served-model-name qwen3.6-27b --enable-prefix-caching --host 0.0.0.0 --port 8000

改完重启 vLLM 容器即可(bash vllm_qwen_run.sh)。


五、通用调试手法小结(问答报错必看)

  1. 抓 Dify 出站消息:在 plugin_daemon 容器内 /tmp/dify_openai_messages.log,不是宿主机 /tmp;改完要 docker exec docker-plugin_daemon-1 cat ...。
  2. 直连模型复现:绕过 Dify,用 curl 直打 vLLM/Xinference,能快速区分"是 Dify 拼消息的锅"还是"模型本身的锅"。
  3. 看容器日志定位插件加载:docker logs docker-plugin_daemon-1 能发现 SyntaxError / import 失败等导致插件整体挂掉的问题。
  4. 改完必须重启:插件代码改动后都要 docker compose restart plugin_daemon;vLLM 参数改动要重启 vLLM 容器。
  5. Agent 应用必须用 streaming 测:用 blocking 模式调 Agent API 会直接被拒,不是业务报错。

六、落地清单(问答报错排查顺序)


风险提示(通用):本文 A、C 涉及的插件补丁与 vLLM 参数,插件升级/重装会覆盖 chat.py 的补丁,

升级后若再次报 System message must be at the beginning,按本文 2.3–2.5 重打一遍即可。

已根治(2026-09-24 起):model 类插件的自动升级已关闭、langgenius/openai 已加入排除名单,

补丁不会再被夜间自动升级冲掉 → 专题十七 · §5 根治。

另注:openai 插件 1.0.5 上游已原生实现单条 leading system 合并(_single_leading_system),

该版本上本补丁已冗余,不要再打,以免和上游逻辑互相干扰。

完整从零部署见 Dify_Qwen安装配置手册_cursor.md;专题一/二/三分别覆盖"接入 Qwen""Xinference 跑 bge-m3""Agent 知识库选不到"。


七、实战记录(本次部署真实遇到的情况)

本次问答报错的 400,就是主案例 A 那一连串排查的真实过程。按统一维度拆解。

现象:vLLM 已起来,在 Agent 里发"描述考勤与请休假管理制度的目的",页面直接返回 400 System message must be at the beginning。

定位(抓出站消息):在 chat.py 临时加调试日志,抓 plugin_daemon 容器内 /tmp/dify_openai_messages.log,发现 Dify 出站是 5 条连续 system(知识库助手提示 + Agent shell 工具提示 + 一条空 system + 配置 + 环境变量),顺序没问题但 Qwen3 拒收多条 system。 完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_08.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_08.sh

bash
#!/usr/bin/env bash
set -euo pipefail

# 加日志位置:client.chat.completions.create(...) 之前
#   with open('/tmp/dify_openai_messages.log','a') as f: f.write(json.dumps([...]))
cd /dify/dify-main/docker && docker compose restart plugin_daemon
docker exec docker-plugin_daemon-1 sh -c 'rm -f /tmp/dify_openai_messages.log'
docker exec docker-plugin_daemon-1 cat /tmp/dify_openai_messages.log

改了什么:把 _ensure_system_first 从早期"只排序"改成"合并"(去空、\n\n 连接成单条),见本文 2.5;并重申该函数必须写在 from __future__ import annotations 之后(否则触发专题八的 SyntaxError)。

怎么判断根因(直连复现):用 curl 直打 vLLM —— 两条 system、含空 system 都报同样 400,合并成单条则成功,确属 Qwen3(qwen3_5 架构)只接受一条 system。

怎么重启服务: 完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_09.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_09.sh

bash
#!/usr/bin/env bash
set -euo pipefail

cd /dify/dify-main/docker && docker compose restart plugin_daemon
sleep 8 && docker ps --filter name=plugin_daemon --format '{{.Names}} {{.Status}}'

怎么看日志: 完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_10.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_10.sh

bash
#!/usr/bin/env bash
set -euo pipefail

docker logs docker-plugin_daemon-1 2>&1 | grep -iE 'error|syntax|traceback' | tail -20   # 无报错即 OK

怎么判断正常(验证):用 Agent API streaming 模式实测: 完整脚本:scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_11.sh

运行/引用:bash /opt/workbuddy/scripts_bash_cursor/Dify_专题四_Agent问答报错/专题四_内联_11.sh

bash
#!/usr/bin/env bash
set -euo pipefail

curl -s -N -X POST http://192.168.110.207/v1/chat-messages \
  -H "Authorization: Bearer app-v9yYVTQTrUoSFHNqAhufSGmh" \
  -H "Content-Type: application/json" \
  -d '{"inputs":{},"query":"描述考勤与请休假管理制度的目的","response_mode":"streaming","user":"debug"}'

流式事件里无 event: error、出现 event: agent_message 且正确检索「播城内部知识库」给出答案 → 通过。

收尾:验证通过后把临时调试日志代码删掉,docker compose restart plugin_daemon 一次,插件恢复干净。

一句话对照:专题四前面是"分层方法与代码",本节是"本次那个真实的 400 是怎么被定位并修掉的"。


本专题 Python 程序一览

| 文件 | 用途 | |---|---|| 专题四_01_加出站调试日志.py | 专题四 · 步骤 1:在 chat.py 注入临时调试日志(验证后请运行删日志脚本)。 | | 专题四_02_读出站消息日志.py | 专题四 · 步骤 2:读取 plugin_daemon 容器内出站 messages 调试日志。 | | 专题四_03_vLLM复现system消息.py | 专题四 · 步骤 3:直连 vLLM 复现多条/空 system 与合并后成功。 | | 专题四_04_合并system补丁_修改后.py | 专题四 · 步骤 4 · 修改后:替换 chat.py 的 _ensure_system_first 为合并逻辑。 | | 专题四_05_删调试日志_修改后.py | 专题四 · 步骤 5 · 修改后:删除 chat.py 临时调试日志块。 | | 专题四_06_Agent流式验收.py | 专题四 · 步骤 6:Agent 公开 API streaming 模式验收(无 event: error)。 |

专题四_01_加出站调试日志.py

运行:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_01_加出站调试日志.py

完整补丁:patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_01.py

运行/引用:见 patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_01.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""专题四 · 步骤 1:在 chat.py 注入临时调试日志(验证后请运行删日志脚本)。"""
from __future__ import annotations

import sys

import _bootstrap  # noqa: F401
import _config as C
import _plugin_patch as P
from dify_api import Dify


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        chat_path = P.find_plugin_file(d, "chat.py")
        print("chat.py:", chat_path)
        content = P.read_remote_file(d, chat_path)
        new_content = P.inject_debug_log(content)
        P.deploy_plugin_file(d, chat_path, new_content)
        d.sh(
            "docker exec %s sh -c 'rm -f /tmp/dify_openai_messages.log && "
            "touch /tmp/dify_openai_messages.log'" % C.PLUGIN_CONTAINER
        )
        print("已重启 plugin_daemon 并清空 /tmp/dify_openai_messages.log")
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

专题四_02_读出站消息日志.py

运行:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_02_读出站消息日志.py

完整补丁:patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_02.py

运行/引用:见 patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_02.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""专题四 · 步骤 2:读取 plugin_daemon 容器内出站 messages 调试日志。"""
from __future__ import annotations

import sys

import _bootstrap  # noqa: F401
import _config as C
from dify_api import Dify


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        out, err = d.sh(
            "docker exec %s cat /tmp/dify_openai_messages.log" % C.PLUGIN_CONTAINER
        )
        text = out or err
        if not text.strip():
            print("[提示] 日志为空。请先在 Agent 里发一条问题触发报错,再运行本脚本。")
            return 1
        print(text)
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

专题四_03_vLLM复现system消息.py

运行:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_03_vLLM复现system消息.py

完整补丁:patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_03.py

运行/引用:见 patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_03.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""专题四 · 步骤 3:直连 vLLM 复现多条/空 system 与合并后成功。"""
from __future__ import annotations

import json
import sys

import _bootstrap  # noqa: F401
import _config as C
from dify_api import Dify


def curl_chat(d: Dify, messages: list[dict], label: str) -> None:
    body = json.dumps({"model": C.LLM_MODEL, "messages": messages})
    remote = "/tmp/vllm_sys_test.json"
    with d.sftp.open(remote, "w") as f:
        f.write(body)
    cmd = (
        "curl -s -X POST %s/v1/chat/completions "
        "-H 'Content-Type: application/json' "
        "-H 'Authorization: Bearer %s' "
        "-d @%s" % (C.VLLM_URL, C.VLLM_KEY, remote)
    )
    out, err = d.sh(cmd, t=120)
    print("\n=== %s ===" % label)
    print(out or err)
    try:
        j = json.loads(out)
        err_msg = j.get("error", {}).get("message") or j.get("message")
        if err_msg:
            print("[error]", err_msg)
    except Exception:
        pass


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        curl_chat(
            d,
            [
                {"role": "system", "content": "A"},
                {"role": "system", "content": "B"},
                {"role": "user", "content": "hi"},
            ],
            "TEST1 两条 system(期望 400)",
        )
        curl_chat(
            d,
            [
                {"role": "system", "content": "A"},
                {"role": "system", "content": ""},
                {"role": "system", "content": "C"},
                {"role": "user", "content": "hi"},
            ],
            "TEST2 含空 system(期望 400)",
        )
        curl_chat(
            d,
            [
                {"role": "system", "content": "A\n\nB\n\nC"},
                {"role": "user", "content": "hi"},
            ],
            "TEST3 合并单条 system(期望成功)",
        )
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

专题四_04_合并system补丁_修改后.py

运行:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_04_合并system补丁_修改后.py

完整补丁:patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_04.py

运行/引用:见 patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_04.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""专题四 · 步骤 4 · 修改后:替换 chat.py 的 _ensure_system_first 为合并逻辑。"""
from __future__ import annotations

import sys

import _bootstrap  # noqa: F401
import _config as C
import _plugin_patch as P
from dify_api import Dify


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        chat_path = P.find_plugin_file(d, "chat.py")
        print("chat.py:", chat_path)
        content = P.read_remote_file(d, chat_path)
        new_content = P.replace_function(content, "_ensure_system_first", P.ENSURE_SYSTEM_FIRST)
        P.deploy_plugin_file(d, chat_path, new_content)
        print("已应用合并 system 补丁并重启 plugin_daemon")
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

专题四_05_删调试日志_修改后.py

运行:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_05_删调试日志_修改后.py

完整补丁:patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_05.py

运行/引用:见 patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_05.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""专题四 · 步骤 5 · 修改后:删除 chat.py 临时调试日志块。"""
from __future__ import annotations

import sys

import _bootstrap  # noqa: F401
import _config as C
import _plugin_patch as P
from dify_api import Dify


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        chat_path = P.find_plugin_file(d, "chat.py")
        content = P.read_remote_file(d, chat_path)
        new_content = P.remove_debug_log(content)
        P.deploy_plugin_file(d, chat_path, new_content)
        print("已删除调试日志并重启 plugin_daemon")
        return 0
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())

专题四_06_Agent流式验收.py

运行:/usr/bin/python3 /opt/workbuddy/scripts_cursor/Dify_专题四_Agent问答报错/专题四_06_Agent流式验收.py

完整补丁:patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_06.py

运行/引用:见 patches_cursor/Dify_专题四_Agent问答报错/专题四_内联_06.py

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""专题四 · 步骤 6:Agent 公开 API streaming 模式验收(无 event: error)。"""
from __future__ import annotations

import json
import sys

import _bootstrap  # noqa: F401
import _config as C
from dify_api import Dify


def main() -> int:
    d = Dify(C.DIFY_EMAIL, C.DIFY_PASSWORD)
    try:
        d.login()
        body = {
            "inputs": {},
            "query": C.AGENT_TEST_QUERY,
            "response_mode": "streaming",
            "user": "debug-workbuddy",
        }
        remote = "/tmp/agent_stream_test.json"
        with d.sftp.open(remote, "w") as f:
            f.write(json.dumps(body, ensure_ascii=False))
        cmd = (
            "curl -s -N -m 420 -X POST %s/v1/chat-messages "
            "-H 'Authorization: Bearer %s' "
            "-H 'Content-Type: application/json' "
            "-d @%s" % (C.DIFY_BASE, C.APP_API_KEY, remote)
        )
        out, err = d.sh(cmd, t=450)
        text = out or err
        print(text[-2000:] if len(text) > 2000 else text)
        if "event: error" in text:
            print("\n[FAIL] 流式响应含 event: error")
            return 1
        if "agent_message" in text or "message" in text:
            print("\n[OK] 流式响应正常")
            return 0
        print("\n[WARN] 未检测到 agent_message,请人工核对上方输出")
        return 1
    finally:
        d.close()


if __name__ == "__main__":
    sys.exit(main())