前一版流程见这里:记一次使用agent在一小时内完整调研上百篇文献的工作流
本文记录的是后续工程化改造。
项目背景是 1999-2025 年共 28 期完整英文技术期刊,文章总量在百篇级。目标不是简单翻译全文,而是围绕某一长期主题建立索引、筛选相关文章、拆分文章级摘录,并继续做翻译和横向梳理。
第一版流程已经能跑通,但后续暴露出两个主要问题:
- MinerU 本机运行慢,环境依赖重,Windows 下还会遇到编码、路径和依赖问题。
- 翻译阶段任务量大,长文需要分块,源文件更新后还要能定向重跑,不能靠人工记忆维护状态。
这次改造主要解决这两点。
MinerU 改为 MCP 调用
原先做法是本机直接调用 MinerU。这个方式适合单次测试,但不适合长期批处理。
主要问题有三个:
- 本机环境重,依赖变动后排查成本高。
- 转换耗时较长,中途异常后不容易判断是真失败,还是 Markdown 和图片已经生成。
- Windows stdout 编码问题会干扰错误判断。
现在改为全局 mineru MCP,项目内只保留一层桥接脚本:
global mineru MCP
-> .codex/scripts/mineru_mcp_client.py
-> output/<issue>/<issue>.md
-> output/<issue>/images/
桥接脚本的职责比较固定:
- 从全局 Codex MCP 配置读取
mineruserver; - 调用
parse_documents; - 强制 UTF-8 环境;
- 统一返回
extract_path和images_dir; - 如果 MCP 没有干净退出,但 Markdown 和 images 已经落盘,则按
fallback_success处理。
这样 MinerU 不再散落在各个 workflow 里,而是变成一个统一的 PDF → Markdown 服务入口。
示例入口:
python .\.codex\scripts\mineru_mcp_client.py `
--source ".\input.pdf" `
--output-dir ".\output\issue-name" `
--language en `
--enable-ocr
期刊批量转换则通过更上层的管理脚本执行:
powershell -ExecutionPolicy Bypass -File .\.codex\scripts\run-issue-mineru.ps1 -FromVol 49 -Progress
这层只负责选择任务、跳过已有 Markdown、是否覆盖旧输出,实际转换仍然走 MCP 桥接脚本。
Runner 改造
早期流程接近 hooks 模式:agent 开始时写状态,结束时收集输出并推进进度。
后来改成普通 runner。原因很简单:Windows 下 hooks 不稳定,而且复杂任务只靠 start/stop 两个事件不够。
当前 runner 的基本生命周期是:
read progress.json
-> select next unit
-> write active-run.json
-> create runs/<workflow>/<timestamp-id>/
-> write prompt.txt
-> codex exec
-> validate output
-> finalize official output
-> update progress.json
-> clear active-run.json
几个状态文件分工如下:
progress.json 业务队列和完成状态
active-run.json 当前锁,防止并发写入
runs/... 单次运行审计目录
progress.md 人类可读的流水日志
每次运行至少保留:
prompt.txt
stdout.txt
stderr.txt
last-message.md
metadata.json
这样中断后可以明确回答几个问题:
- 当前跑到哪个 unit;
- child process 是否真的完成;
- 输出是否通过校验;
- finalize 是否写入正式产物;
- progress 是否推进;
- 是否可以 repair 或定向重跑。
通用命令保留为固定形态:
node .codex/runner/exec.mjs <workflow> --status
node .codex/runner/exec.mjs <workflow> --dry-run
node .codex/runner/exec.mjs <workflow> --once
node .codex/runner/watch.mjs <workflow> --once
node .codex/runner/exec.mjs <workflow> --repair
实际使用时,一般先 --dry-run 看下一个任务,再 --once 验证一次完整链路,最后才批量跑。
翻译 Workflow
翻译阶段是这套 runner 的主要压力测试。
长文不能直接整篇塞进一个 codex exec,所以按 Markdown 结构切分:
article.md
-> chunk01.md
-> chunk02.md
-> chunk03.md
每个 chunk 单独作为一个 unit:
chunk01.md -> codex exec -> chunk01.zh-CN.md
chunk02.md -> codex exec -> chunk02.zh-CN.md
chunk03.md -> codex exec -> chunk03.zh-CN.md
全部 chunk 完成后,再合并为正式中文稿:
chunk*.zh-CN.md -> article.zh-CN.md
这里重点不是“切块”,而是状态判断。
之前遇到过一个问题:源 Markdown 已经更新,但旧中文稿还在。如果 runner 只判断“译文文件是否存在”,就会把旧译文误判为已完成。
现在的规则改成:
先比较 sourceHash
再判断旧输出是否可复用
也就是说,输入指纹变化优先于输出存在性。
如果源文变了,对应 unit 会重新进入 pending。只有该 unit 自己成功 finalize 后,才更新它的 last successful sourceHash。
这个规则解决了三类问题:
- 源文更新后旧译文挡住新任务;
- chunk 边界变化后旧 chunk 被错误复用;
- 某个 unit 完成时误清理其他 unit 的重跑状态。
定向重跑也固定成三步:
node .codex/runner/exec.mjs component-translate --dry-run
node .codex/runner/exec.mjs component-translate --once
node .codex/runner/exec.mjs component-translate
先确认目标范围,再验证一个前台 unit,最后跑完整队列。
当前状态
当前项目状态如下:
主期刊处理:28/28 completed
文章级/分块翻译:161/161 translated
基础文献翻译:37 translated + 2 skipped_existing_cn
专题来源翻译:54/54 translated
这些数字来自状态文件和 watcher 输出,不依赖对话上下文。
对应状态入口包括:
output/progress.json
output/timeline-progress.json
translation-progress.json
runner watch output
结论
这次改造的核心不是 prompt,而是控制面。
对批量文献任务来说,agent 只应该处理当前最小单元。其余状态必须外置:
- 队列状态;
- 当前锁;
- 输入指纹;
- 失败记录;
- 正式输出;
- 单次运行审计目录。
MinerU MCP 解决输入转换的一致性问题。
Runner 解决批量任务的恢复、监控和重跑问题。
翻译 workflow 则验证了长文分块、输入变更检测和正式产物合并是否可靠。
这套结构跑起来之后,后续扩展新的文献队列或新的处理阶段,主要工作就变成新增 workflow adapter,而不是重新设计整条流水线。
1 个帖子 - 1 位参与者