跳到主要内容

第 5 章:从研究脚本到能上线的服务

第 1 章deepanalyze.py 是个 150 行的研究脚本:裸 exec、无文件管理、无产物收集。要给真用户用,得补上文件、隔离、流式、产物、报告。本章看生产那一面:API/demo/chat_v2/

5.1 OpenAI 兼容 API:同样的循环,加固版

API/chat_api.py:chat_completions 是核心端点。骨架和第 1 章完全同构,只是每一步都加了料:

收到请求(messages + file_ids)
→ 建/取一个 thread,对应一个工作目录 workspace_dir
→ 把上传的文件拷进 workspace
→ prepare_vllm_messages 拼 # Instruction / # Data
→ while not finished:
流式调 vLLM(stop_token_ids,边收边往前端推 SSE)
见 </Answer> → finished
见完整 <Code>...</Code> → 抽码、执行、diff 收产物、回灌 <Execute>
→ 末尾生成报告文件、回最终 chunk(含 thread_id + 生成文件 URL)

几个生产级增强:

(a) thread = 工作目录。 每个会话一个 thread_id,映射到一个独立 workspace(get_thread_workspace)。带上 thread_id 再请求就复用同一目录,实现多轮带状态的对话。

(b) 文件上传走 OpenAI files 风格。POST /v1/filesfile_id,再在 chat 里带 file_ids;端点把文件从存储拷进 workspace(chat_api.py:104-114)。防覆盖靠 chat_api.py:113if not os.path.exists(dst_path)——已存在同名就跳过拷贝。注意这里调的 uniquify_path(API/utils.py:42-44)当前是 no-op(return target,直接返回原路径),并不真去重命名;真正避免覆盖的是那句 os.path.exists 判断,别把 uniquify_path 当成防重名机制。

(c) 截断补救更稳。 流式时若 vLLM finish_reason=="stop" 但代码没闭合,手动补 </Code> 再执行(chat_api.py:184-191);没有代码段就判 finished。比参考版只靠 stop="</Code>" 更鲁棒。

(d) 自动注入中文字体。 执行前给代码拼一段 plt.rcParams['font.sans-serif']=['SimHei'](chat_api.py:29-33198),让画的图能正常显示中文。

5.2 产物追踪:diff 工作目录

模型跑代码会产出新文件(图片、清洗后的 CSV、模型文件)。怎么知道哪些是这次新生成的?WorkspaceTracker(API/utils.py:353 起)用快照 diff:

# 示意,提炼自 API/utils.py:WorkspaceTracker
before = snapshot(workspace) # {路径: (大小, mtime)}
... 执行模型代码 ...
after = snapshot(workspace)
new_or_changed = after - before # diff_and_collect 返回新增/改动的文件

每轮执行后 tracker.diff_and_collect() 拿到新产物,render_file_block 把它们转成可下载链接塞进回复(chat_api.py:200-205)。这样前端就能展示「本轮生成了 chart.png」。

5.3 报告导出

循环结束后 generate_report_from_messages(API/utils.py:412 起)把整段对话里的标签段抽出来,拼成一份 Markdown 报告落盘到 workspace/generated/,再返回下载链接。extract_sections_from_history 负责把 <Analyze>/<Code>/<Execute>/<Answer> 各段组织成正文 + 附录。fix_tags_and_codeblock(API/utils.py:232-252)还会补救未闭合的标签和奇数个 ``` 围栏——容错流式截断。

5.4 v2 的关键升级:Docker 沙箱

Web v2(demo/chat_v2/)把执行从「子进程」升级成「每会话一个 Docker 容器」,这是安全上的实质改进。核心在 docker_executor.py:

ensure_execution_backend_ready(session_id):
容器在跑? → 复用
容器存在没跑? → docker start
镜像不存在? → 报错(要先 build Dockerfile.exec)
都没有? → docker run -d,把 workspace 挂进去,keepalive 常驻

execute_python_in_docker(script, ...):
docker exec <容器> python <脚本> # 在容器里跑,带超时

设计要点:

  • 会话级容器复用:容器名按 session_id 派生(_container_name_for_session),同会话多轮共用一个容器,省去反复启停。
  • workspace 挂载:-v {workspace_root}:{docker_workspace_dir}(docker_executor.py:184-188),容器里跑的代码读写的就是宿主的工作目录,产物追踪照常工作。
  • 常驻 + 空闲回收:容器用 while true; do sleep 3600; done 保活(_keepalive_command),超过 idle_ttl 没用的容器被 _cleanup_idle_session_containers 清掉。
  • 保活进程级超时:docker exectimeout=timeout_sec,超时返回 [Timeout]: execution exceeded N seconds(docker_executor.py:255)。

这一步把「在服务器主机上 exec 任意模型代码」这个大隐患,收进了容器边界——对一个会自己写代码的 agent 来说是必须的。

5.5 四种 UI 速览

DeepAnalyze 同一套后端循环挂了四个前端:

UI形态位置
WebUINext.js 前端 + Python 后端,上传文件做深度研究demo/chat/
WebUI v2更顺滑 + HeyWhale API + Docker 沙箱demo/chat_v2/
JupyterUI把标签段转成 Jupyter 的 Markdown/Code cell 真执行demo/jupyter/
CLIRich 美化的命令行,支持中英文demo/cli/

它们差别只在前端和执行后端,agent 循环和标签协议完全一致——再次印证核心在模型、外层可换。

5.6 评测 playground

playground/ 收了一套统一的 vLLM 评测,覆盖多个数据科学 benchmark:DSBench(data_analysis / data_modeling)、DS-1000TableQADABStep-Research。每个 benchmark 下都有一份 run_deepanalyze.py + 一个本地版 DeepAnalyzeVLLM(和参考实现同构),按各 benchmark 的答案格式接评分。想复现论文数字从这里入手。

5.7 代码地图

主题文件符号
生产 API 端点API/chat_api.pychat_completions
安全执行(临时文件+子进程)API/utils.pyexecute_code_safeexecute_code_safe_async
产物 diff 追踪API/utils.pyWorkspaceTrackerdiff_and_collect
报告导出API/utils.pygenerate_report_from_messagesextract_sections_from_history
标签容错修补API/utils.pyfix_tags_and_codeblock
Docker 沙箱demo/chat_v2/backend_app/services/docker_executor.pyensure_execution_backend_readyexecute_python_in_docker
v2 聊天服务循环demo/chat_v2/backend_app/services/chat.pystop_token_ids=[151676,151645]
评测脚本playground/DSBench/data_analysis/run_deepanalyze.pyDeepAnalyzeVLLM