数据截至 (上游 commit 0b54741848df)
第 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/files 拿 file_id,再在 chat 里带 file_ids;端点把文件从存储拷进 workspace(chat_api.py:104-114)。防覆盖靠 chat_api.py:113 的 if 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-33、198),让画的图能正常显示中文。
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 exec带timeout=timeout_sec,超时返回[Timeout]: execution exceeded N seconds(docker_executor.py:255)。
这一步把「在服务器主机上 exec 任意模型代码」这个大隐患,收进了容器边界——对一个会自己写代码的 agent 来说是必须的。
5.5 四种 UI 速览
DeepAnalyze 同一套后端循环挂了四个前端:
| UI | 形态 | 位置 |
|---|---|---|
| WebUI | Next.js 前端 + Python 后端,上传文件做深度研究 | demo/chat/ |
| WebUI v2 | 更顺滑 + HeyWhale API + Docker 沙箱 | demo/chat_v2/ |
| JupyterUI | 把标签段转成 Jupyter 的 Markdown/Code cell 真执行 | demo/jupyter/ |
| CLI | Rich 美化的命令行,支持中英文 | demo/cli/ |
它们差别只在前端和执行后端,agent 循环和标签协议完全一致——再次印证核心在模型、外层可换。
5.6 评测 playground
playground/ 收了一套统一的 vLLM 评测,覆盖多个数据科学 benchmark:DSBench(data_analysis / data_modeling)、DS-1000、TableQA、DABStep-Research。每个 benchmark 下都有一份 run_deepanalyze.py + 一个本地版 DeepAnalyzeVLLM(和参考实现同构),按各 benchmark 的答案格式接评分。想复现论文数字从这里入手。
5.7 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 生产 API 端点 | API/chat_api.py | chat_completions |
| 安全执行(临时文件+子进程) | API/utils.py | execute_code_safe、execute_code_safe_async |
| 产物 diff 追踪 | API/utils.py | WorkspaceTracker、diff_and_collect |
| 报告导出 | API/utils.py | generate_report_from_messages、extract_sections_from_history |
| 标签容错修补 | API/utils.py | fix_tags_and_codeblock |
| Docker 沙箱 | demo/chat_v2/backend_app/services/docker_executor.py | ensure_execution_backend_ready、execute_python_in_docker |
| v2 聊天服务循环 | demo/chat_v2/backend_app/services/chat.py | stop_token_ids=[151676,151645] |
| 评测脚本 | playground/DSBench/data_analysis/run_deepanalyze.py 等 | DeepAnalyzeVLLM |