24 KiB
技能并发与流式修复:配置、前端包与验收说明
日期: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,
两者任选其一即可。开启线程池开关后,按以下顺序取第一个非空值:
DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE(正式变量)DEERFLOW_IO_THREADS(兼容别名)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. 前端替换与回退
- 先备份服务器当前前端目录和
runtime-config.js,并备份实际后台 .env/config.yaml。 - 校验下载 ZIP 的 SHA-256 与本说明记录一致,再解压到一个新的发布目录。
- ZIP 包含
dist/;Web 根目录应指向其内部,确保根下直接能找到index.html,不要误放成dist/dist/。 - 保留/恢复服务器自己的
runtime-config.js。核对生产后台地址、认证和嵌入参数;不要把构建机默认值当生产配置。 也不要覆盖生产 .env 的密钥与数据库配置。恢复 runtime-config 后其单文件哈希自然与原包不同。 - 更新后台代码并按需设置四个开关,使用现有部署方式重启后台,再切换前端发布目录。
- 刷新浏览器缓存后检查普通问答、智能体问答、技能示例、读取技能、生成文件以及离开后重新打开正在生成的会话。
- 如需回退性能优化,后台关闭相应开关并重启,无需删除技能文件或技能缓存目录。 如需回退模型协议或前端修复,须恢复对应的已备份代码/前端发布目录,单独关闭性能开关不够。
正式 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 项记录时序断言、既有 Wikiauto_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 个独立会话逐级测试:
- 纯聊天和包含技能读取/写文件的问答分开测试,保持提示长度和输出规模接近真实业务。
- 每级记录成功/失败/超时数、首字和总耗时的分位数,以及技能
lock_wait_ms/total_ms。 - 同步查看模型端 429/503、排队数量/首 token 延迟,以及应用 CPU、内存、磁盘和数据库等待。
- 若上游只能并行处理少量请求,应用扩线程无法增加上游容量;应控制并发或扩容模型服务。
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