Files
deerflow-code/docs/部署说明-技能并发与流式修复-20260927.md
T

24 KiB
Raw Blame History

技能并发与流式修复:配置、前端包与验收说明

日期:2026-09-27(2026-09-28 补充高并发运行配置)。

本说明对应本次提交的修改,包含模型流式兼容与恢复、React #185 错误处理和已完成的最小技能示例, 以及可选的技能视图复用、版本缓存、模型计时日志队列和 Gateway 线程池扩容。 部署前通过 git log -1 核对代码提交;仅修改配置或替换旧包不能确认已部署本次修复。

1. 先确认需要更新什么

  • 后台代码也要更新:四个新开关需要新版后台读取。只换前端 dist 或只把字段写进旧版 config.yaml 不会实现后台优化。
  • 前端:使用本说明末尾的 ZIP,里面是完整 dist/,不是源码或 node_modules。
  • 仓库运行配置已开启优化:两份 config.yaml 的四个开关均已显式设为 true。但已有服务器的持久化/外挂配置不会被仓库自动覆盖,仍需手工同步。
  • 开启线程池开关后可将上限调整为 100,但不提高模型服务商配额、不启用多 worker。

2. .env 需要修改哪些项

后台启动所使用的 .env 保持或修改为:

# Gateway 进程数,不是线程池大小;当前内存流/运行管理器必须单进程。
GATEWAY_WORKERS=1

# 默认就是 0;显式写 0 可防止误用不安全的多进程覆盖开关。
DEERFLOW_ALLOW_UNSAFE_MULTIWORKER=0

# Gateway asyncio 线程池上限;还必须打开下一节的 asyncio_thread_pool_enabled。
DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE=100

适用于已更新的 scripts/start_backend-online.sh 和 offline-backend-20260512/start_backend.sh。 若通过自定义服务直接运行 Uvicorn,也应确认实际命令是单 worker;仅修改 .env 不会改变硬编码的启动命令。 启动环境中已导出的同名变量可能覆盖 .env,需一并核对。

前面三个技能/日志优化没有独立环境变量,它们配置在下一节的 config.yaml。 线程池上限变量现已真正接入;修改 .env 后必须重新启动后台,Docker 应通过现有启动脚本重新创建容器, 单纯 docker restart 不会更新容器创建时注入的环境变量。 其余模型地址、密钥、数据库连接等保留服务器原值,不用本机 .env 覆盖生产文件。

100 个线程与环境变量优先级

GATEWAY_WORKERS=100 不是把工具线程池调成 100,而是启动 100 个进程,当前部署不要这样设置。

DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE=100 已经实现,兼容别名 DEERFLOW_IO_THREADS=100, 两者任选其一即可。开启线程池开关后,按以下顺序取第一个非空值:

  1. DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE(正式变量)
  2. DEERFLOW_IO_THREADS(兼容别名)
  3. config.yaml 的 runtime_performance.asyncio_thread_pool_size(默认 100)

有效范围为整数 1..1024。选中环境变量的值非法时启动失败并提示,不会静默退回默认小线程池。 开关关闭时不读取环境变量来改变线程池,保留 Python 原默认。已有正式变量时,修改别名不会覆盖它。 线程按需创建,100 是上限,不会预先常驻启动 100 个线程。

3. config.yaml 新增配置

在实际加载的 config.yaml 顶层配置这一段,不能缩进到 models、sandbox 或 skills 内, 也不要重复定义两个 runtime_performance。下面是程序在字段缺失时的兼容性默认值; 当前仓库的两份可运行配置已使用后文的高并发开启值:

runtime_performance:
  # 一次工具调用内只获取一次已验证技能视图,默认 false。
  skill_view_reuse_enabled: false

  # 版本未变时复用技能视图,默认 false。
  skill_view_cache_enabled: false

  # 模型计时日志使用独立队列,不争抢耗时工具线程,默认 false。
  model_timing_nonblocking_enabled: false

  # 是否覆盖 Gateway asyncio 默认线程池,默认 false。
  asyncio_thread_pool_enabled: false
  asyncio_thread_pool_size: 100

如决定启用本次四项优化并将线程池上限设为 100,改为:

runtime_performance:
  skill_view_reuse_enabled: true
  skill_view_cache_enabled: true
  model_timing_nonblocking_enabled: true
  asyncio_thread_pool_enabled: true
  asyncio_thread_pool_size: 100

四个开关独立生效,可以单独开关。关闭/省略对应项时,对应的性能优化走原实现。 这四项不控制模型协议兼容修复或前端错误处理;全部关闭不等于回退所有代码。 为了让已有模型实例和所有后台进程配置一致,修改后重启后台;回退也是改回 false 后重启。

确认没有改错配置文件

  • 已设置 DEER_FLOW_CONFIG_PATH 时,以它指向的文件为准;Docker 内该值必须是容器内可访问路径。
  • 本仓库包含 offline-backend-20260512/config.yaml 和 offline-backend-20260512/backend/config.yaml; 两份已同步为高并发开启值,但服务器要修改的仍是启动时实际加载/挂载的那一份。
  • 启动日志 Loaded AppConfig from ... 可确认路径,RUNTIME_PERFORMANCE 可确认实际开关值。

4. 开启后做了什么,没做什么

单次调用复用

同一次 read_file、ls、glob、grep、bash 等工具执行期间,技能检查、沙箱初始化、 路径替换和输出脱敏共用一份已验证视图;退出调用后清理作用域,下一次调用重新验证。 不会把一个问答最初的技能启停状态一直沿用到结束。

技能版本缓存

每次核对启停 JSON 内容、已知源/目标文件和目录的元数据及标记。 版本一致时不重新发现/解析全部技能、不复制文件、不持有全局刷新锁。 新增、编辑、删除、启停及支持文件变化触发重建,并保留跨进程锁合并并发刷新。

没有采用会延迟下线的 TTL 缓存;下一次工具调用能发现下线或无效配置并拒绝访问。 已经开始执行的工具不因此被强行中断。 缓存仅用于标准 LocalSkillStorage,自定义存储实现/子类保守回到原刷新路径。 同一 Gateway 进程内,仅在多个热命中调用真正重叠时合并为一次元数据校验; 该批调用结束后结果立即丢弃,新调用仍重新校验。启停文件哈希不同的调用不会加入旧校验, 校验异常会对该批调用统一 fail-closed,且不会阻止后续调用重试。

热路径仍有 O(文件数) 元数据校验,慢磁盘/网络挂载仍可能耗时;这不是“所有文件操作零开销”, 也没有把受锁保护的重建变成无锁写入。

模型计时日志

仅轻量计时/入队在回调中执行,日志 I/O 由独立线程消费,不占工具共享线程池或事件循环。 队列上限 4096 条;日志端长期阻塞时会丢弃诊断并在恢复后汇总告警,不无限积压或阻塞模型输出。 退出时最多等待 0.5 秒尽力刷新,异常退出可能丢失尾部日志。

这是 ModelTimingCallback 的优化,不会改变其他回调(如 RunJournal、可选追踪插件)、 子智能体调度池、数据库池或模型上游的连接/速率限制。不能据此保证 100 个会话同时生成。

Gateway 工具线程池

开启 asyncio_thread_pool_enabled 后,在后台启动时、LangGraph 初始化之前设置当前 Gateway 事件循环的默认执行器。同步工具、该循环的 asyncio.to_thread 等可使用最多 100 个槽位。 关闭时完全不替换默认执行器;退出时由拥有该事件循环的服务运行器清理。

这不是 AnyIO 的同步 HTTP 路由线程池,也不改变子智能体专用池或独立事件循环的默认池。 线程池扩大只能减少可并行 I/O 的排队,不能让同一把技能锁同时被多人持有,也不会增加模型端的生成配额。

5. 日志检查

确认加载日志类似:

RUNTIME_PERFORMANCE skill_view_reuse_enabled=True skill_view_cache_enabled=True model_timing_nonblocking_enabled=True asyncio_thread_pool_enabled=True asyncio_thread_pool_size=100

线程池最终是否生效,以另一条启动日志为准(进程 id 随实际启动变化):

GATEWAY_ASYNCIO_EXECUTOR enabled=true max_workers=100 source=DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE pid=12345

如果仍显示 enabled=false max_workers=python_default,说明线程池开关没打开或改错了配置文件, 不是已经调整到 100。改环境变量后若日志仍取旧值,检查是否重建了 Docker 容器及实际 env-file。

查看以下日志:

日志 用途
GATEWAY_ASYNCIO_EXECUTOR 实际线程池开关、最终上限、配置来源和进程 id;关闭时显示使用 Python 默认,不虚报 100
SKILL_VIEW_CACHE status=refreshed 发生重建;包含 lock_wait_ms、refresh_ms、total_ms 和条目数
SKILL_VIEW_CACHE status=joined 等待期间其他线程/进程已完成重建,本次直接复用
SKILL_VIEW_CACHE status=hit 缓存命中;正常命中为 DEBUG,超过 500ms 的慢命中为 INFO
SKILL_VIEW_CACHE status=failed 刷新失败,旧能力不可继续使用,需要排查配置/磁盘
LLM_CONNECT_START/OK、LLM_REQUEST_DONE/FAILED 模型调用生命周期、首块和总耗时
LLM_CALL_RETRY/FAILED、OPENAI_TOOL_STREAM_SUMMARY 模型重试/兼容处理诊断
LLM_TIMING_LOG_DROPPED 日志队列曾满;检查日志磁盘/采集端速度

如需查看正常缓存命中,可短时设置顶层 log_level: debug;排查后恢复 info。 首块耗时不等于纯网络连接等待,仍包括上游排队、模型处理等阶段。单次 curl 成功也不能证明并发容量足够。 本次日志不记录技能正文、模型正文或 API 密钥。

6. 前端替换与回退

  1. 先备份服务器当前前端目录和 runtime-config.js,并备份实际后台 .env/config.yaml。
  2. 校验下载 ZIP 的 SHA-256 与本说明记录一致,再解压到一个新的发布目录。
  3. ZIP 包含 dist/;Web 根目录应指向其内部,确保根下直接能找到 index.html,不要误放成 dist/dist/。
  4. 保留/恢复服务器自己的 runtime-config.js。核对生产后台地址、认证和嵌入参数;不要把构建机默认值当生产配置。 也不要覆盖生产 .env 的密钥与数据库配置。恢复 runtime-config 后其单文件哈希自然与原包不同。
  5. 更新后台代码并按需设置四个开关,使用现有部署方式重启后台,再切换前端发布目录。
  6. 刷新浏览器缓存后检查普通问答、智能体问答、技能示例、读取技能、生成文件以及离开后重新打开正在生成的会话。
  7. 如需回退性能优化,后台关闭相应开关并重启,无需删除技能文件或技能缓存目录。 如需回退模型协议或前端修复,须恢复对应的已备份代码/前端发布目录,单独关闭性能开关不够。

正式 Linux AMD64 离线镜像/发版仍通过独立的 knowledge-base-bj/deerflow-offline-deployment 仓库流程执行;本次前端 ZIP 不是完整后台离线镜像。

7. 本地验证结果与限制

后台

  • 提交前最终专项:技能/线程池/日志/启动配置及技能老回归 166 项通过;模型适配、错误分类、 诊断失败保护、SSE 响应头、会话计时与真实 transport 123 项通过(两组不重叠,共 289 项)。 对应修改模块与测试 Ruff 通过,harness 未增加对 app 的反向依赖。后续没有执行生产模型并发压测。
  • 本次技能复用/缓存/配置、日志回调与相关技能目录、大文件安全专项:79 项通过; 又补充 1 项开关关闭/重新开启的回退测试,通过缓存文件全部 13 项回归,合计 80 项。
  • 模型工具参数流/推理流、错误恢复、SSE 传输和启动 worker 校验另 76 项通过;相关专项共 156 项通过。
  • 后续新增线程池接入与配置专项 45 项通过(与前述配置测试有重叠,不相加充当独立总数): 覆盖默认关闭、两个环境变量优先级/别名、非法值拒启、启动顺序,以及实际 24 / 100 个本地阻塞任务同时执行、 退出后线程全部清理;新模块和相关测试 Ruff 检查通过。另一次联合技能/日志/启动配置回归 88 项通过。 该测试只验证本地线程池,不是 100 路模型压测。
  • 线程池接入后再次尝试后台全量测试(首次失败即停止):604 通过、2 跳过、1 失败, 仍停在修改前已复现的 test_llm_metrics.py::test_middleware_records_success_with_speed,不是全量通过。
  • 冷启动 8 线程并发只构建一次;真实第二进程复用持久化清单、不再发现技能。
  • 下线/无效配置、源文件变更、同长保留 mtime 编辑、目标篡改、Windows junction 保护均有回归覆盖。
  • 真实 LangChain 异步模型测试证明:默认工具线程池占满且日志处理器阻塞时,启用计时日志优化仍可开始、输出 token 并结束;未调用真实模型服务。
  • 修改后尝试全量测试,设置失败 5 项停止:871 通过、2 跳过、5 失败,并非全量通过。 失败包括 test_llm_metrics.py 的 3 项记录时序断言、既有 Wiki auto_sync 配置与测试预期不一致、 test_model_management.py 的 thinking 参数预期不一致。首项在本轮修改前即已复现; 本轮没有修改指标中间件、模型工厂或 Wiki auto_sync 设置,不为过测试擅改这些业务配置。
  • 本次针对 #185/MD 的追加回归:模型真实 SDK 回放、工具参数、思考字段、日志、配置、线程池、 错误中间件和传输联合 145 项通过。其中新增 SDK 回放 19 项,覆盖 8 种写文件形态, 确认 JSON 尚未闭合时部分 MD 已传出、最终工具名称和参数正确;没有连接线上模型。

合成性能对比

Windows 11 / Python 3.12.13,本机临时目录:150 个技能、450 个文件,各模式预热一次后串行 10 次。 旧刷新平均约 3133ms,缓存命中平均约 806ms(本次约 3.89 倍); load_skills 从 10 次降为 0 次。临时数据已清理。 这不是生产 Linux 性能结论,也不是多人访问模型的承载压测。

前端与交付包

  • 提交前最终前端专项 60 项通过(原 57 项,加同 reader 续接/新页面重播与迟到事件隔离 2 项、 最小技能示例 1 项);真实生产 React 浏览器错误边界测试再次通过。
  • 最终全量类型检查仍有既有 75 个诊断,本次变更文件无新增诊断;记录在 .runtime/frontend-final-typecheck-20260927.log。不要把构建成功等同全量类型检查通过。
  • 前端本次流式恢复、React #185 分类、技能示例、流式预览及相关权限等 32 项专项通过; 修正新增测试文件一处 .ts 导入导致的 TS5097 后,相关 16 项再次通过。
  • 最新追加前端专项 57 项通过,含真实 SDK SSE → 文件参数 → MD 预览;旧 16ms 设置在连续流中 没有通知刷新,修复后在 JSON 尚未闭合时持续更新。真实 Chrome + 生产 React 中用循环 layoutEffect 触发 #185,验证静态友好兜底、无自动重挂、点击重新载入后恢复。
  • 最新 pnpm build 通过:Vite 13,853 个模块,8 分 49 秒;存在大于 500KB 的 chunk 提醒,不是构建失败。 构建日志 .runtime/frontend-final-build-20260927.log;本次修复重新打开会话的游标边界后已重新打包。
  • 前一版临时 localhost 静态 HTTP 烟测 通过:入口 HTML 及引用的主 JS/CSS/runtime 等 5 个资源均 200, 响应字节 SHA-256 与磁盘一致,没有缺失资源;烟测服务已关闭。
  • 最新 ZIP 中 886 个文件均逐文件比对大小和 SHA-256,与本次 dist 一致;入口/CSS 的 99 个本地资源引用全部存在。包内无 .env, 入口为 dist/index.html,包含 dist/runtime-config.js。
  • 全量 pnpm typecheck 未通过:复测还有 75 个诊断、涉及 29 个本次未修改文件, 包括现有 nextra / @codemirror/state 依赖、旧测试及 legacy QA 类型问题; 本次新增测试文件的 TS5097 已修正并复测,本次前端变更文件无类型诊断。 完整本地日志为 .runtime/qa/frontend-delivery-typecheck-20260927.log; 不能把生产构建通过表述成整个项目类型检查通过。 追加修复后本次产品变更文件无全量类型诊断,新增测试及依赖专项 TypeScript 检查为 0 诊断。
  • 附加 legacy Chrome CSS 检查 未通过,报三个本轮未修改文件 legacy-html-preview.test.ts、legacy-html-preview.ts、SolutionProjectLanding.tsx 中的现代颜色函数字样; 没有报本次 dist CSS 缺失回退,也不能据此宣称完成旧浏览器端到端兼容验收。

交付 ZIP(仓库根目录):

deerflow-frontend-dist-20260927-master-final.zip
  • ZIP 大小:35,869,210 字节(约 34.2 MiB)。
  • 此包替代 deerflow-frontend-dist-20260927-191207-185-stream-fix.zip 及更早版本;旧包保留,但不包含最终的游标作用域修复。
  • 解压后文件总大小:66,347,693 字节。
  • ZIP SHA-256:
7E4C1525BDEB686161FFF5AED12D36087407BBF4F472B87E78CA04DADD1DF371

Linux 可用 sha256sum deerflow-frontend-dist-20260927-master-final.zip; Windows 可用 Get-FileHash -Algorithm SHA256 .\deerflow-frontend-dist-20260927-master-final.zip。

未连接生产环境进行真实多人模型问答端到端验收,不能宣称所有线上卡顿已被实测消除。

8. 关于 20 人同时使用

当前不能承诺 20 人同时问答一定流畅。本次验证了本地代码路径和构建产物,并没有验证你的两台服务器、 上游模型地址/API key 配额、数据库/磁盘及实际技能耗时在 20 路负载下的容量。

部署新版后台、明确开启所需开关后,先在测试环境按 5 → 10 → 20 个独立会话逐级测试:

  1. 纯聊天和包含技能读取/写文件的问答分开测试,保持提示长度和输出规模接近真实业务。
  2. 每级记录成功/失败/超时数、首字和总耗时的分位数,以及技能 lock_wait_ms/total_ms。
  3. 同步查看模型端 429/503、排队数量/首 token 延迟,以及应用 CPU、内存、磁盘和数据库等待。
  4. 若上游只能并行处理少量请求,应用扩线程无法增加上游容量;应控制并发或扩容模型服务。

20 个在线用户、20 条运行中的问答和 20 个正在模型端生成的请求不是同一指标。

9. #185 与 MD 流式追加修复

用户已确认思考模式正常;本节针对仍出现的 #185 和 MD 预览卡住。

  • 四个直接流入口统一使用 STREAM_RENDER_BATCHING=true,避免安装版本 SDK 的数字 16ms 尾部防抖不断推迟 UI 刷新;保留异步批处理,不改成每个片段同步强制渲染。
  • #185 流错误不会再按模型异常直接显示原始错误串;同一轮最多一次自动恢复,重复失败暂停重连, 不主动取消后台任务,不循环重新发送问题。友好提示 30 秒去重。
  • 真正渲染/commit 阶段的 #185 由页面错误边界接住,进入静态提示,只能手动重新载入; 不吞其他错误。控制台 react_ui_render_error 保留组件堆栈,帮助定位未知的线上循环组件。
  • 恢复游标与当前页面的消息累积器绑定:同一页面断线续接不重复拼接工具参数,离开后重新打开或 切换会话则完整重播,不能复用全局旧游标跳过正文前缀;旧 reader 的迟到事件也不污染新页面。
  • 后端兼容重复工具名称,避免 write_filewrite_file 使工具路由和预览识别失效; OPENAI_TOOL_STREAM_SUMMARY repeated_tool_names 可确认是否命中。也修复了空 error:{} 被误判失败。
  • 诊断日志写入失败不会覆盖真实模型异常、取消或成功结果。错误脱敏后仍在内存中识别上下文超限, 并识别 invalid_api_key 等结构化认证错误,防止本应修剪上下文或停止重试的请求反复排队。
  • 实时预览和物理落盘不同:模型还在生成参数时展示虚拟 MD 内容;参数完整、工具校验通过后, write_file 才正式写沙箱文件。此次不把未完成的 JSON 直接写进最终文件。

本地已经复现并修复上述具体缺陷,但没有线上原始流/组件堆栈,不能保证它们解释全部生产故障。

10. threads 创建慢:数据库路由与阶段耗时

POST /api/threads 会先查/写应用数据库的 threads_meta,再调用检查点存储写入空 checkpoint; 它不需要请求模型,也不会在创建时遍历所有旧问答。

当前仓库部署模板是 database.backend: mysql,同时 checkpointer.type: postgres、 checkpointer.use_postgres: false。如果服务器保持这些值且没有 DEER_FLOW_USE_PG_CHECKPOINTER 覆盖,实际是 MySQL 元数据 + SQLite 检查点,清理 PostgreSQL 不会加速这条链路。 本地开发配置则是 SQLite。线上以实际加载的配置和环境覆盖为准,不凭数据库连接串是否存在判断。

新建/搜索接口新增 GATEWAY_OPERATION_TIMING 日志和 Server-Timing 响应头, 浏览器 Network 的响应头也能看到,不必先拿服务器日志:

  • metadata_lookup / metadata_create:应用元数据查询/写入。
  • checkpoint_create:检查点创建,包含 saver 锁/连接池等待和数据库写入,不能等同为纯 SQL 时间。
  • metadata_search:会话列表的元数据查询。
  • handler_total:进入处理函数后的耗时;不含进入函数前的事件循环/鉴权/代理排队。

模型图构建仍保持原来的任务上下文。新增 AGENT_BUILD_TIMING:超过 500ms 以 WARNING 记录, 正常为 DEBUG。已在本地复现同步慢工厂阻塞同事件循环的其他请求,但不能直接裸搬到线程: 延迟工具注册上下文需回传,MCP 初始化存在跨事件循环约束。此处目前是定位日志,不是已经消除阻塞。

本机安装的 SQLite saver 和 PostgreSQL saver 都存在实例级串行锁;后者使用连接池也不代表 同一个 saver 自动并行处理所有检查点。扩大 asyncio 线程池不解除此锁。 尚未取得线上分阶段耗时,因此不能承诺新建 10 秒已经根治,也不能承诺归档即可解决。 新增计时与真实 SQLite 锁测试、已有分析项目/浏览器内部创建入口联合 43 项通过, 其中计时专项 14 项;这是本地回归,不是线上延迟验收。

11. 30 天已结束会话的 PostgreSQL 归档 SQL

归档脚本单独交付,不进入本源码仓库,也不在启动时执行。使用 pg_checkpoint_archive.sql 搬移, pg_checkpoint_restore.sql 按提交批次恢复;两者需与 pg_checkpoint_common.sql 放在同一目录。 应用数据库在 MySQL / PostgreSQL 的候选只读导出分别为 mysql_checkpoint_candidates.sql / pg_checkpoint_candidates.sql。

按用户确认,候选仅包括 30 天未更新、所有运行已结束的会话,排除正在运行、待处理和等待用户输入的会话。 PG 检查点没有应用会话终态字段,因此不能只按 checkpoint.ts 猜测,必须先从应用元数据导出候选 ID。 脚本仅在确认实际启用 PG、全部写入服务已停、备份可恢复及双读回退关闭后允许提交。 默认是预览/回滚;提交时三张原始表的全部 namespace 一起搬入新归档 schema,逐行校验后才删源行, 恢复时不覆盖同 ID 的新数据,归档副本保留。不删除沙箱文件。

范围与风险:这是 PG 检查点冷归档,不是完整应用归档。它不搬 MySQL threads_meta/runs, 因此旧会话列表入口可能仍在,但归档正文需要恢复后才能读取/续聊。归档到同一库的新表也不保证减少总磁盘占用。 SQL 的验证记录与具体执行命令见单独交付包的 README;部署前仍须在生产备份恢复的隔离副本演练。 不要跳过预检直接在生产提交。如果线上仍为 SQLite,这组 PG SQL 不适用。

独立包 deerflow-db-archive-20260927.zip 含 SQL、README 和复验测试,不含数据库数据、连接凭据或数据库二进制。 已在 PostgreSQL 16.15 + 安装的 LangGraph checkpoint 3.1.2 原始建表结构上通过 13 项真实执行和 14 项静态检查; 未执行真实 MySQL 候选导出,详见独立包 README。ZIP 共 9 个文件、20,983 字节,SHA-256:

E90A189DA7D7032527EED7CEC9B51DE1A30FB6D48B4382CE06AF1C287B670F59