wth2jhl.online

Back

MathModelAgent 把数学建模做成一条 `/1start-mathmodel` 命令MathModelAgent 把数学建模做成一条 `/1start-mathmodel` 命令

MathModelAgent 把数学建模做成一条 /1start-mathmodel 命令

全国大学生数学建模竞赛、国赛、美赛、华数杯、华为杯——三天三夜、一篇能提交的论文、一份要跑出来的代码、一摞能用的图表。这件事的痛点二十年来没变过:它从来不是「模型不会建」,而是「时间不够」。

jihe520/MathModelAgent 是一个专门为这件事设计的 Agent 系统。它把整条流水线切成了 6 个可独立调用的 Skill,再由后端的 4 个 Agent 协作执行——你只需要在 Claude Code / Codex 里敲一句 /1start-mathmodel 完成这个建模任务,它会自动跑完分析、建模、编码、绘图、流程图、论文撰写和验收,产出一份可直接提交的 PDF。

MathModelAgent 项目主页和桌面版入口

本文是 2026-09-13 这个 41MB / 966 文件 / 6 个 SKILL + 4 个 Agent / 17 套模板 / 9 步验收的项目的一次完整源码级深拆。我会带你看:作者两年前从自研 Agent 框架到完全迁到 Harness+SKILL 的转向、Skill 与 Agent 的边界怎么划、Agent 基类怎么在 token 撑爆前”安全切割”压缩记忆、Coder 怎么反思重试、Writer 怎么调 OpenAlex 搜文献、取消信号怎么贯穿整个异步链,以及一个让 Agent 开发者眼前一亮的工程套路清单。

1. 项目定位与全景#

MathModelAgent 的 README 第一行就立了 Flag:

专为数学建模设计的 Agent,自动完成数学建模,生成一份完整的可以直接提交的论文。

它在 GitHub 上的形态是一组可独立安装的 SKILL 包,通过 npx skills add jihe520/MathModelAgent --all 安装,运行在 Claude Code / Codex / Pi 等任意兼容 AgentSkills 规范的 Harness 上。同时也提供了桌面版(Electron + 内置 Claude Code,macOS 已 Developer ID 签名并通过 Apple 公证,Windows 上线中)和 WebUI(Vue3 + FastAPI + Redis)。

剥掉所有外壳,这个项目做的事情可以收成一句话:把数学建模竞赛的全流程(读题 → 拆题 → 建模 → 写代码 → 跑代码 → 出图 → 写论文 → 验收)切成一组有明确边界、可独立调用的 Skill,后端用 4 个 Agent 协作执行,最终交付一份可提交的论文 PDF。

它给出的特性也是这张流水线的展开:

  • 17 套 Typst + LaTeX 双引擎论文模板(国赛 / 美赛 / 华数杯 / 华为杯 / APMCM / MCM / ICM 等)
  • 9 步自动验收(文本泄漏检测 → 数值一致性 → 编译 → PDF 可视化)
  • 6 种 HIL 决策动作(confirm / edit / regenerate / ask / skip / abort)
  • Code Interpreter 双形态:本地 Jupyter(代码存成 notebook 方便再编辑)+ 云端 E2B / Daytona
  • multi-llms:每个 Agent 可以单独配置最合适的模型,底层走 LiteLLM 兼容 100+ provider
  • Web Search(Tavily) + RAG 知识库(ChromaDB + Rerank,建模方法/代码模板/论文写作参考)
  • 多 Agent:Coordinator(协调)/ Modeler(建模)/ Coder(写代码)/ Writer(写论文)
  • 四层容错:有限重试 → Fallback Hand Off → Evaluator Shadow Mode → Feedback Rerun
  • 多入口:桌面版 / WebUI / CLI(master 分支)/ SKILL(main 分支,2026 年的主推形态)

读完这份 README,我最感兴趣的不是某个具体特性,而是作者在 Thinking 段写下的两句话:

两年前,我都是自己实现一套 Agent 框架,现在和以后更多的 Agent 产品直接基于 Harness 如 Codex / Claude Code / Pi + SKILLS 来构建。

项目以后只会做 SKILLS 层的迭代和优化,不会再做其他部分。

这两句话是整个项目最重要的工程信号。MathModelAgent 不是又一个 Agent 框架,它是一个”Harness 之上的 Skill 教材”。作者把整个 Agent 框架里所有可重用的部分(消息协议、Provider 抽象、Tool Call 协议、记忆压缩、取消信号)都内化在了 SKILL 里——你不需要再写一份框架,你只需要在支持 AgentSkills 的 Harness 里加载这套 SKILL。

2. 6 步 SKILL Pipeline:Harness 上的”流程管道”#

整个项目的目录结构是这种形状:

skills/
├── 1start-mathmodel/   # 入口:询问偏好 + 生成 plan.md + 串联各阶段
├── 2analysis-modeling/ # 赛题分析 + 建模设计 → reports/ANALYSIS_MODELING_REPORT.md
├── 3coding-visual/     # 编程实现 + 数据图表 → code/ + figures/*.pdf + RESULTS_REPORT.md
├── 4drawio/            # 非数据图(技术路线图 / 流程图 / 架构图)→ figures/*.drawio + PDF
├── 5writing/           # 撰写论文(双引擎)→ paper/main.typ 或 main.tex
├── 6verity/            # 验收:文本门禁 + 编译 + PDF 检查 → reports/VERIFY_REPORT.md
├── doctor/             # 环境检查
├── mathmodel-figure-templates/  # 科研图表模板(sci-box 姊妹项目)
├── typst-author/       # Typst 知识助手(独立 skill)
└── _references/        # 数学建模规范知识库(只读)
text

每个 Skill 都是一份标准的 agentskills.io ↗ 格式文件——SKILL.md 包含 name / description / allowed-tools,正文写工作流、产出、阶段边界和质量要求。Harness(Claude Code、Codex、Pi)加载后会按 Skill 的 frontmatter 注册为可调用命令,正文按需加载到 LLM 上下文。

Skill 之间不互相 import,只通过文件约定通信——这是它和传统 Python 包最大的区别:

阶段Skill读写
1. 入口1start-mathmodel用户输入plan.md, todo.md
2. 分析2analysis-modelingplan.mdreports/ANALYSIS_MODELING_REPORT.md
3. 代码3coding-visualANALYSIS_MODELING_REPORT.mdcode/, figures/*.pdf, RESULTS_REPORT.md
4. 流程图4drawio前三份 reportfigures/*.drawio, DRAWIO_REPORT.md
5. 论文5writing所有 report + figurespaper/main.typ 或 main.tex
6. 验收6verity所有产物reports/VERIFY_REPORT.md

这种”文件系统作为 Skill 间 API”的设计有个隐藏好处:你可以在断网的情况下,手动把中间产物修改一下,然后从断点继续跑剩下的 Skill。后端 4 个 Agent 也只是”在 Harness 上实现 Skill 描述的工作流”——它没有跨 Skill 的强耦合逻辑。

2.1 入口 Skill 的”询问与决策”#

1start-mathmodel 这个 Skill 很有意思——它不干活,只问问题:

## 工作流

### 1. 询问用户偏好 AskUserQuestions

在规划前,只询问会实质影响流程的问题。问题要少而关键。

优先询问(按重要性排序):

1. 排版引擎:Typst 还是 LaTeX?
2. 竞赛类型:国赛/华为杯/华中杯/MCM/...
3. 论文语言:中文/英文
4. 子问题数量是否已知

将用户的选择记录到 plan.md 的"方案"小节中。
markdown

然后根据用户的回答,生成 plan.md 和 todo.md,再依次调用 2 → 3 → 4 → 5 → 6。整个流程在 todo.md 里就是 5 行 checkbox:

# 待办事项
- [ ] 1. 赛题分析与建模设计 - `2analysis-modeling`
- [ ] 2. 编程实现和图表生成 - `3coding-visual`
- [ ] 3. 流程与架构图绘制 - `4drawio`
- [ ] 4. 竞赛论文撰写 - `5writing`
- [ ] 5. 验证和验收 - `6verity`
markdown

每完成一个阶段,就在对应行打勾。这是一个简单但有效的进度持久化机制——下次 Session 重启时,只要读 todo.md 就能知道当前在哪一步。

3. 后端 4-Agent:角色、Prompt、协作#

后端是基于 FastAPI + Redis + LiteLLM 实现的 4-Agent 协作系统,代码集中在 backend/app/core/ 下,核心 2406 行:

backend/app/core/
├── agents/
│   ├── agent.py             # Agent 基类(302 行):对话历史 / 轮次控制 / 记忆压缩
│   ├── coordinator_agent.py # Coordinator(74 行):意图识别 + 子问题拆解
│   ├── modeler_agent.py     # Modeler(119 行):数学建模
│   ├── coder_agent.py       # Coder(219 行):代码生成与执行 + 反思
│   └── writer_agent.py      # Writer(187 行):论文撰写 + 文献搜索
├── llm/
│   ├── llm.py               # LLM 封装(205 行):多 Provider + 重试 + 工具调用
│   ├── llm_factory.py       # LLMFactory(57 行):按 Agent 类型分配模型
│   ├── providers/           # OpenAI Chat / OpenAI Responses / Anthropic
│   └── types.py             # StandardResponse 统一返回类型
├── prompts/                 # 各 Agent 的系统提示词
├── flows.py                 # 编排逻辑(179 行)
└── workflow.py              # 主流程入口(243 行)
text

3.1 工作流入口:workflow.py 的”舞台调度”#

workflow.py 里的 MathModelWorkFlow.execute() 是整个系统的总指挥。打开它,你会看到一个非常清晰的”舞台调度”代码:

async def execute(self, problem: Problem):
    # 1. 预校验 4 个 Agent 的 model 和 api_key 配置,缺一就拒
    # 2. LLMFactory 创建 4 个独立 LLM 实例(每个 Agent 配不同模型)
    # 3. CoordinatorAgent 跑意图识别 + 子问题拆解 → questions dict
    # 4. ModelerAgent 跑数学建模 → 假设 + 目标函数 + 约束 + 求解策略
    # 5. 创建 code_interpreter(local jupyter)+ scholar(OpenAlex 文献)
    # 6. 创建 CoderAgent 和 WriterAgent
    # 7. 进入 solution_flows 循环(每个子问题):Coder 跑 → Writer 写
    # 8. 进入 write_flows 循环(摘要 + 引言 + 结论等):Writer 写
    # 9. user_output.save_result() 落盘
python

最关键的是第 6 步:4 个 Agent 在循环外一次性创建好,在循环里复用同一个 chat_history(各自维护)。这意味着 Coder 在写问题 1 代码时积累的对话历史,会作为”上下文”带到问题 2——这是为什么 Coder 能保持前后逻辑一致的根本原因。

而中间每一步都有 await self._check_cancelled(),配合 redis_manager.publish_message(...) 实时把进度推到前端 WebSocket。这是数学建模场景下用户对长任务必须可见可中断的硬需求——24 小时的任务,你不可能等它跑完再看。

3.2 角色拆解:从 Prompt 看”分工”#

4 个 Agent 的系统提示词(backend/app/core/prompts/*.py)决定了它们的实际行为:

  • Coordinator(25 行 prompt): 负责”意图识别 + 拆题”。读题面,识别子问题数量,生成结构化的 questions dict——{"ques1": "...", "ques2": "..."}
  • Modeler(150 行 prompt): 负责”建立数学模型”。对每个子问题输出 ModelerToCoder,包含假设、变量、目标函数、约束、求解算法、代码实现要点
  • Coder(225 行 prompt): 负责”写代码 + 跑代码”。默认使用 LiteLLM 的 tool call 协议调 execute_code 工具,在 Jupyter kernel 里执行,有错误就反思重试
  • Writer(220 行 prompt): 负责”写论文”。除了写,还负责用 search_papers 工具调 OpenAlex 检索真实文献插入引用,以及按章节插入 coder_response.created_images 返回的图片

这 4 个 Agent 不是”协作完成任务”那种平行关系,而是严格串行的接力——Coordinator 不知道 Modeler 的存在,Modeler 不知道 Coder 的存在,Coder 不知道 Writer 的存在。它们唯一的接口是 UserOutput 这个中间产物:

user_output = UserOutput(work_dir=self.work_dir, ques_count=self.ques_count)
# ...
user_output.set_res(key, writer_response)  # 把每段写好的论文塞进去
# ...
user_output.save_result()  # 最终落盘
python

这种”窄接口 + 文件系统即上下文”的设计让 4 个 Agent 完全可以独立替换、独立测试、独立调优——这就是为什么项目能稳定迭代两年。

4. 核心设计 1:Agent 基类的”安全切割”记忆压缩#

这是我读完 backend/app/core/agents/agent.py 后觉得整个项目最值得学的工程细节。302 行的 Agent 基类,核心就解决一个问题:当对话历史撑爆上下文窗口时,怎么压缩,才不会破坏 tool call 序列的完整性?

OpenAI / Anthropic 的对话 API 有个铁律:如果你想让 role=tool 的消息作为某个 assistant 调用的结果出现,那条 assistant 消息里必须有对应的 tool_calls 字段,反之亦然。换句话说,对话历史里的 tool 消息和它的 tool_call 必须是连续的。随便从中间切一刀,API 就会报 messages: tool_calls ids didn't match 错误。

很多 Agent 框架的做法是”无脑丢老消息”——先把最早的系统消息留住,然后从前往后丢到 token 阈值以内。但 MathModelAgent 用了更精细的策略。

4.1 三段式压缩:compress_if_needed#

_find_safe_preserve_point 是关键。它从后往前扫描,找到最后一个”从此处切下去,后面的消息里没有孤立 tool”的位置:

def _find_safe_preserve_point(self) -> int:
    min_preserve = min(3, len(self.chat_history))
    preserve_start = len(self.chat_history) - min_preserve
    
    for i in range(preserve_start, -1, -1):
        if self._is_safe_cut_point(i):
            return i
    return len(self.chat_history) - 1  # 兜底:至少保留最后一条
python

_is_safe_cut_point 检查从 i 开始的所有消息,如果遇到 role=tool 的消息,就向前找对应的 tool_call_id 在 start_idx 之前出现过——没有就说明是孤儿,这个切点不安全,继续往前找。

4.2 三段兜底:失败也不让对话崩坏#

LLM 总结本身可能失败(网络/限流/超长 prompt),所以 Agent 基类还设计了三层 fallback:

  1. 首选:用 LLM 总结历史
  2. 次选:_get_safe_fallback_history —— 不总结,只保留 system + 最后 N 条非 tool 消息(从后往前找,直到切点安全为止)
  3. 最差:只保留最后一条非 tool 消息(对话退化,但 API 不会报错)

这种**“LLM 总结失败也不让对话结构破坏”** 的设计,是 Agent 在生产环境长期跑 24 小时任务最关键的鲁棒性——你的代码必须保证 LLM 怎么抽风,对话历史都不会让下一次 API 调用直接 400。

4.3 取消信号:asyncio.wait 的 FIRST_COMPLETED 套路#

Agent 基类还埋了一个不起眼但极其重要的细节:_chat 方法的可中断等待。

asyncio.wait(... FIRST_COMPLETED) 会在两个任务任一完成时立即返回——只要 cancel_event 一 set,正在跑的 LLM 调用就会被立刻取消,而不是等它跑完再判断。这对前端”停止按钮”的体验至关重要:用户点停止,1 秒内整个工作流停掉,而不是等 Claude 把当前 token 吐完再退出(可能 30 秒)。

这种”取消语义贯穿 chat / agent / workflow 三层”的设计,是 Agent 长任务 UI 体验的分水岭。

5. 核心设计 2:Coder 的反思重试 + Writer 的文献 tool call#

5.1 Coder:execute_code + 反思 prompt#

Coder Agent 的核心循环非常清晰——生成代码 → 调 execute_code 工具在 Jupyter kernel 里执行 → 拿到执行结果 → 如果报错,生成反思 prompt → 重新调用 LLM 让它改代码 → 直到跑通。

两个细节值得拎出来:

第一,没有用”循环调 LLM → 让它自己决定 retry”,而是把”是否 retry”这个决策显式写在 Python 代码里。LLM 只负责”反思”——是否继续交给 retry_count 计数器和 max_retries 边界控制。这避免了一个常见坑:LLM 在反思 prompt 里”自信地说”已修复但其实没修,导致循环跑空。

第二,成功路径不 return,而是 continue——让 LLM 决定是否还有下一步。这给了 LLM 自主决定”这个子问题是否已经做完”的灵活性(比如画完图之后还要不要算下统计指标),但又通过 current_chat_turns 做了硬边界保护:

if self.max_chat_turns is not None and self.current_chat_turns >= self.max_chat_turns:
    raise Exception(f"Reached maximum number of chat turns ({self.max_chat_turns}). Task incomplete.")
python

max_retries(反思次数)和 max_chat_turns(总对话次数)两个独立计数器,前者管错误恢复,后者管总消耗。这是”任务级 budget”和”操作级 budget”分离的标准做法。

5.2 Writer:Anthropic/OpenAI 双 tools + search_papers#

WriterAgent 比 Coder 更复杂——它需要写论文,还要搜文献,所以有两套 tool:

api_type = self.model.api_type
tools = writer_tools_anthropic if api_type == ApiType.ANTHROPIC else writer_tools
python

Anthropic 的 tool use 协议(2024 年 11 月起)和 OpenAI 不完全一致:Anthropic 用 input_schema,OpenAI 用 parameters。所以同一份 tool 描述要在两边各写一份。这个细节暴露了一个事实:LiteLLM 帮你屏蔽了 chat 协议,但 tool call schema 还是要自己分版本。

search_papers 是 WriterAgent 最关键的 tool——它在写某个章节前会先调一下,拿到真实文献:

if response.tool_calls and response.tool_calls[0].name == "search_papers":
    query = json.loads(response.tool_calls[0].arguments)["query"]
    papers = await self.scholar.search_papers(query)  # OpenAlex
    papers_str = self.scholar.papers_to_str(papers)
    await self.append_chat_history({"role": "tool", "tool_call_id": tool_id, "name": "search_papers", "content": papers_str})
    
    # 再调一次 LLM,把文献塞进上下文,让它继续写
    next_response = await self._chat(history=self.chat_history, tools=tools, tool_choice="auto")
    response_content = next_response.content or ""
python

注意这里有个特别细节:Writer 在 tool_call 拿到文献后必须再调一次 LLM,因为第一轮 LLM 只产出了 tool_call 还没写正文。这是 tool call 协议的标准模式——但很多 Agent 实现者在第一轮拿到 tool_call 后就直接 return,导致后续 LLM 没机会看到 tool 结果。MathModelAgent 这里处理得很干净。

5.3 OpenAlex 替代 Tavily:开源学术搜索的务实选择#

README 里有一行 TODO 注释很扎眼:

<!-- NOTE: 原计划 Tavily API 未实现,当前使用 OpenAlex 替代 -->
- [ ] web search tool: Tavily API 搜索互联网获取真实数据
markdown

backend/app/tools/openalex_scholar.py 实现了一个对 OpenAlex ↗(一个开源的学术元数据索引)的薄封装——它免费、无 API Key 也能用,有 Key 的话能拿到更高的 rate limit。对于数学建模这种”找真实文献”的需求,OpenAlex 比 Tavily 更专业(Tavily 是通用 Web 搜索)。

这是工程上很务实的一次”原型替换”——Tavily 适合搜”今年 iPhone 销量”,OpenAlex 适合搜”ARIMA 在能源预测里的应用”。

6. 核心设计 3:多 Provider 抽象 + Code Interpreter 双形态#

6.1 LLM Provider 抽象:同一接口三种协议#

backend/app/core/llm/providers/ 实现了三种 LLM 协议适配:

  • OpenAIChatProvider:经典的 /v1/chat/completions(兼容 100+ provider)
  • OpenAIResponsesProvider:新的 /v1/responses(支持 reasoning_content、tool_calls 结构不同)
  • AnthropicProvider:Anthropic Messages API(input_schema 风格)

抽象接口是 BaseProvider.call(messages, model, api_key, base_url, tools, tool_choice, max_tokens, top_p) -> StandardResponse。返回的 StandardResponse 把三种协议的差异抹平:

@dataclass
class StandardResponse:
    content: str | None           # 文本回复
    reasoning_content: str | None # 思考过程(DeepSeek / o1)
    tool_calls: list[ToolCall]    # 工具调用
    usage: Usage                  # token 用量
python

切换 Provider 只用改 LLMFactory 里的配置——Coder 想用 GPT-4o,Writer 想用 Claude Sonnet 4.5,Coordinator 想用 DeepSeek,Modeler 想用 GLM,这些都靠 api_type 字段分派。这种设计在 2024 年底到 2025 年很常见(参考 Pi Agent Harness ↗ 的 pi-ai 同样模式),但 MathModelAgent 在 SKILL 框架下做到了”换 Provider 不换 Skill”。

6.2 Code Interpreter 双形态:本地 Jupyter + 云端 E2B/Daytona#

代码执行是个有状态的活——不是简单的 subprocess.run(),它需要变量持久化、状态连续、能把图存到指定目录。backend/app/tools/ 里的代码解释器设计成了抽象工厂:

code_interpreter = await create_interpreter(
    kind="local",          # 或 "e2b" / "daytona"
    task_id=self.task_id,
    work_dir=self.work_dir,
    notebook_serializer=notebook_serializer,
    timeout=3000,
)
python
  • local:基于本地 Jupyter kernel,代码块逐步执行,执行历史存成 .ipynb 文件方便用户再编辑
  • e2b:E2B 云端 sandbox,适合需要更强算力或隔离环境的场景
  • daytona:Daytona 沙盒,同上

这种”本地 + 云端 sandbox 双形态”的模式,在 2025 年的 Agent 产品里几乎是标配(E2B 早期投资人里就有 LangChain 的 Harrison Chase)。它解决的是”代码执行不能污染本机环境”这个生产环境核心问题——Agent 跑用户代码,99% 时间没事,但那 1% 的 rm -rf / 足以搞挂生产。

6.3 Redis + WebSocket:实时进度推送#

整个系统用 Redis 做两件事:任务状态持久化 + 进度广播。redis_manager.publish_message(task_id, SystemMessage(content="建模手开始建模ing...")) 这类调用在工作流里到处都是,WebSocket 路由(/ws/task_id)订阅同一 channel 推到前端 Vue。

这套”Redis pub/sub + WebSocket 桥接”的模式是 FastAPI 长任务可视化的标准答案。优点是水平扩展友好(加 Redis cluster 就行),缺点是断线后会丢消息——所以前端还要靠 GET /api/task/{id}/history 拉一次快照补齐。

7. 求解 + 写作的”二阶段编排”:flows.py 里的 9 段式结构#

backend/app/core/flows.py 是工作流的”细节编排”,把整个论文产出分成两段:

求解段(get_solution_flows)——按固定顺序产出 EDA + 每个子问题 + 灵敏度分析:

def set_flows(self, ques_count: int):
    seq = [
        "firstPage",            # 首页信息
        "RepeatQues",           # 问题重述
        "analysisQues",         # 问题分析
        "modelAssumption",      # 模型假设
        "symbol",               # 符号说明
        "eda",                  # 探索性数据分析
        *ques_str,              # ques1, ques2, ques3, ...
        "sensitivity_analysis", # 灵敏度分析
        "judge",                # 模型评价
    ]
    self.flows = {key: {} for key in seq}
python

求解段的每一项都通过 coder_agent.run(prompt=...) + writer_agent.run(writer_prompt) 双 Agent 串行执行——Coder 先跑出代码和图,Writer 基于结果写这一段的论文。

写作段(get_write_flows)——在求解段之后,补上摘要、结论、参考文献、附录等不需要重新跑代码的部分,只用 Writer Agent。

write_flows = flows.get_write_flows(user_output, config_template, problem.ques_all)
for key, value in write_flows.items():
    writer_response = await writer_agent.run(prompt=value, sub_title=key)
    user_output.set_res(key, writer_response)
python

这种”先求解后写作”的拆法有个隐藏好处:用户在跑完求解段之后,可以中途修改 coder 的代码或结果,只重跑写作段,不必从拆题重来。WebUI 正是靠这个机制实现”HIL 编辑”决策——用户在 Coder 跑出问题 1 后,觉得问题 2 的图不好看,可以直接编辑图片重新生成,只让 Writer 重写相关段落。

整个编排没有用任何工作流框架(Airflow / Temporal / Prefect),纯 Python async 循环。对一个 24 小时内的任务来说,这是过度工程化的反义词——简单可调试,作者在 README 里有句很坦诚的话:

💰 成本低:workflow agentless,不依赖 agent 框架

这话解释了为什么整个项目能稳定两年——没有框架升级带来的破坏性变更。

8. Typst + LaTeX 双引擎:17 套模板的工程化#

5writing Skill 里有一行很关键的约定:

撰写论文前必须让用户选择排版引擎。引擎决定后续所有步骤(模板路径、章节文件扩展名、图片插入语法、编译命令),选错会导致整篇论文格式错误。

两个引擎的”路径选择”硬约定:

Typst  → templates/<lang>/<竞赛>/main.typ          编译命令 typst compile main.typ
LaTeX  → templates/<lang>/<竞赛>-latex/main.tex   编译命令 xelatex -interaction=nonstopmode main.tex(跑两次)
text

支持的竞赛模板(Typst + LaTeX 双版本全覆盖):

  • 中文:apmcm, changsanjiao, cumcm(国赛), diangongbei, dongsansheng, huashubei(华数杯), huaweibei(华为杯), huazhongbei(华中杯), mathorcup, mcm, shuweibei, stats, wuyibei(五一杯)
  • 英文:apmcm, default, mcm(美赛)

华为杯/华中杯/五一杯这些名字容易混,项目用统一别名 huaweibei / huazhongbei / wuyibei 收口。

而图片插入语法也要按引擎切换——Typst:

#figure(image("../../figures/xxx.pdf", width: 85%), caption: [...])
typst

LaTeX:

\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{../../figures/xxx.pdf}
\caption{...}\label{fig:xxx}
\end{figure}
latex

这套”双引擎双模板”的工作量很大,但回报也很大——2024 年起 Typst 崛起,很多选手想换 Typst 不想用 LaTeX,但模板生态跟不上。MathModelAgent 直接给你 17 套现成的两种格式,这种”工程红利”在 GitHub 上很难找。

9. 9 步验收 + 6 种 HIL 决策:Agent 不孤军#

9.1 6verity 的 9 步自动验收#

最后一道 Skill 6verity 的存在让整个系统闭环。它的入口脚本 skills/6verity/scripts/writing_check.sh 按引擎自动选择检查逻辑:

bash "$SCRIPT_PATH" \
  --paper-dir "$PAPER_DIR" \
  --main "$MAIN_FILE" \
  --sections-dir "$SECTIONS_DIR" \
  --references "$REFERENCES_FILE" \
  --figures-dir "$FIGURES_DIR" \
  --results-file "$RESULTS_FILE" \
  --problem-analysis "$PROBLEM_ANALYSIS_FILE" \
  --all-results "$ALL_RESULTS_FILE" \
  | tee _tmp/writing_check.log
bash

脚本只扫描文本,不重跑实验、不编译 PDF。它的检查覆盖:

  1. 章节数量和标题顺序(include 顺序 vs 文件名前缀)
  2. 文本泄漏检测(占位符、内部文件路径)
  3. 图表引用完整性(文中提到的图是否存在)
  4. 数值一致性(RESULTS_REPORT.md 里的数字 vs 论文里的数字)
  5. Typst/LaTeX 编译(typst compile / xelatex 跑两遍)
  6. PDF 可视化检查(pdfinfo 验证页数正常)
  7. 参考文献格式
  8. 代码可复现性(code/ 目录能否重新运行)
  9. 提交清单(题目要求的所有材料是否齐全)

9.2 HIL 人机协同:6 种决策动作#

README 里有一个未完成的功能值得展开:

- [ ] human in loop (HIL): 关键节点暂停等待用户审批,支持 6 种决策动作
  <!-- TODO: 数据模型已实现,但工作流集成未完成 -->
  (confirm / edit / regenerate / ask / skip / abort)
markdown

这套设计非常合理:让用户在关键节点(比如 Coder 跑出问题 1 后)暂停,做 6 种决策:

  • confirm: 接受当前结果,继续下一步
  • edit: 编辑当前产物(改图、改代码段),再继续
  • regenerate: 整个重新跑这一步
  • ask: 反问 LLM 一个具体问题,拿到答案再决定
  • skip: 跳过这一步(比如灵敏度分析不写了)
  • abort: 整个任务终止

这是 Agent 走向生产的关键设计——纯自动跑下来,用户很难放心;但全程每步都让用户确认又太累。只在”关键节点”做 HIL 是工程上的甜区。目前数据模型实现了(schemas/A2A.py 里的 WriterResponse、CoderToWriter),但工作流里还没接——这是 README 标注的 TODO。

10. 四层容错:渐进式降级#

README 列出的容错体系:

🛡️ 四层容错:有限重试 → Fallback Hand Off → Evaluator Shadow Mode → Feedback Rerun

层做什么代码在哪
1. 有限重试单次 LLM 调用失败,指数退避重试LLM.chat(max_retries=...)
2. Fallback Hand Off主模型失败自动切备用模型LLMFactory 配置
3. Evaluator Shadow Mode后台跑第二个模型评分,不阻塞主流程TODO
4. Feedback Rerun评估器打分低 → 把反馈注入 prompt 重跑TODO

注意第 3、4 层都是 TODO——README 列出来了但没实现。这和 Pi Agent Harness 的”早期就上 Pi-ai 多 provider + 后期才做 evaluation”是同一种节奏:先把骨架打通,再补鲁棒性。但这里把”四层容错”明明白白写在 README 里,是一种对用户的预期管理——告诉用户”我知道现在不完美,但这是路线图”。

11. 数据流图与可视化:从一张图看完整流水线#

把前面所有内容浓缩成一张图,就是 MathModelAgent 的本质:

这张图就是 6 个 SKILL、4 个 Agent、3 层可视化入口的完整映射。中间产物(plan / report / paper)全部是文件系统里的 Markdown 或 PDF,没有任何内部状态藏在数据库里——这就是**“开源 + 可调试 + 可接力”** 的本质。

12. 文档、UI 与可观察性:工程化的最后一公里#

12.1 WebUI:Vue3 + Pinia + shadcn-vue + WebSocket#

前端是个标准的现代 Vue3 工程:

  • 状态管理:Pinia stores(对话历史、任务状态、用户配置)
  • UI 库:shadcn-vue(components/ui/ 不要修改,这是 shadcn-vue 生成代码)
  • WebSocket 客户端:utils/websocket.ts 订阅 Redis 推送的进度消息
  • 页面结构:pages/chat/(对话交互)、pages/task/(任务进度)、pages/example/(历史案例)、pages/login/

12.2 desktop 版:Electron + 内置 Claude Code#

README 顶部就推桌面版:

桌面版已内置 Claude Code 与全套 MathModelAgent SKILLS,无需安装 Python / Node.js / Redis,也无需手动配置 SKILL,装好填一个模型 API Key 即可开始建模。

桌面版把后端 + 前端 + Claude Code 全打包进去,用户不需要懂环境配置。macOS 版还做了 Developer ID 签名 + Apple 公证。Windows 版因为代码签名证书没搞定,所以会有 SmartScreen 提示——README 里特意教了”更多信息 → 仍要运行”。

这套”桌面版优先 + 开源版次之”的策略,说明作者的核心用户群是参赛学生——他们没时间配环境,装了就能用最重要。

12.3 CLAUDE.md:把 AI 编程 Agent 当成协作者#

仓库根的 CLAUDE.md 写得很细——这不是写给人类看的,是写给 Claude Code 自己看的:

这套”AI 编程协作者”的指令清单(命令、代码风格、边界)在 2025 年越来越常见——把 AI 当成团队成员,就要像 onboarding 新人一样给它写文档。MathModelAgent 在这一点上做得很完整,甚至还配了一个 .claude/hook_lint.sh 做 Edit/Write 后自动 ruff + biome 检查。

13. 可复用的工程套路清单#

读完全部源码,我自己提炼了一份”给 Agent 开发者的可复用套路清单”,每条都能直接搬到别的项目:

13.1 SKILL 取代 Harness:“Skill 是文件,不是 import”#

不要在 Python 里 import 别的 Skill,而是让 Skill 通过文件系统通信。
plaintext

这让 Skill 可以独立版本化、独立替换、独立测试——你可以把某段 Skill 完全重写,只要文件格式不变,别的 Skill 不受影响。

13.2 Skill 间 API 用”约定文件”#

不要用 RPC、不要用消息队列,直接用 Markdown 文件:

写读
plan.md1start-mathmodel → 2analysis-modeling
ANALYSIS_MODELING_REPORT.md2analysis-modeling → 3coding-visual
RESULTS_REPORT.md3coding-visual → 5writing
figures/*.pdf3coding-visual / 4drawio → 5writing
VERIFY_REPORT.md6verity → 用户

文件系统即 Skill API,这条规则比任何抽象都稳。

13.3 Agent 基类必备 3 件事#

每个写 Agent 框架的人都应该有的 3 件事:

  1. 记忆压缩的安全切割:不要无脑丢老消息,要保证 tool_call 序列不切断
  2. 取消信号的可中断等待:asyncio.wait(FIRST_COMPLETED) 双任务模式
  3. 失败兜底的三段降级:LLM 总结失败 → 安全 fallback → 只保留最后一条非 tool 消息

13.4 Coder Agent 的”反思+重试”硬边界#

if retry_count >= max_retries: return error_response  # 操作级 budget
if current_chat_turns >= max_chat_turns: raise  # 任务级 budget
python

LLM 只负责反思,Python 代码控制是否 retry——避免 LLM 在反思 prompt 里”自信地说”已修复但其实没修,导致循环跑空。

13.5 Tool Call 协议要分 Provider#

Anthropic 用 input_schema,OpenAI 用 parameters,新 Responses API 又不一样——不要指望 LiteLLM 帮你处理 tool schema 差异。把 tools 按 Provider 类型分开,代码会清晰很多:

tools = writer_tools_anthropic if api_type == ApiType.ANTHROPIC else writer_tools
python

13.6 多 Provider 抽象的关键是返回类型统一#

不管协议怎么变,你的 Agent 代码只面对一种返回:

StandardResponse(content, reasoning_content, tool_calls, usage)
python

这是 Pi-ai 和 MathModelAgent 都用的同一招,返回类型统一比调用统一更重要——LLM 的输入可以随 Provider 变,但 Agent 的输入永远是这一个 shape。

13.7 Code Interpreter 一定要”本地 + 云端 sandbox”#

永远不要 subprocess.run(user_code)。本地 Jupyter kernel + 云端 E2B/Daytona 双形态是 2025 年的标准答案。99% 的时间没事,1% 的 rm -rf / 足以搞挂生产。

13.8 WebUI 实时进度:Redis pub/sub + WebSocket 桥接#

FastAPI 长任务的标准答案。redis_manager.publish_message(task_id, message) 在工作流里到处调,WebSocket 路由订阅同一 channel 推到前端 Vue3。

13.9 HIL 决策动作做成”数据模型 + TODO”#

把 HIL 6 种决策(confirm / edit / regenerate / ask / skip / abort)做成数据模型(在 schemas/A2A.py),但工作流暂时不接——先把 schema 定下来,后面慢慢接 UI。这种”先定协议,后定集成”的节奏,可以避免大改。

13.10 README 的 TODO 注释是”用户预期管理”#

- [ ] web search tool: Tavily API 搜索互联网获取真实数据
  <!-- NOTE: 原计划 Tavily API 未实现,当前使用 OpenAlex 替代 -->
markdown

把”未实现”明明白白写出来,比假装实现了更好。这让用户对项目状态有真实预期,不会以为你是骗子。

14. 已知短板与未完成项#

源码级深拆就要诚实。下面这些都是 README 或代码里明确标记的 TODO,未来这个项目能不能从”工具”走向”产品”,就靠这些:

状态模块备注
✅6 步 SKILL pipeline完整可用
✅4 Agent 协作Coordinator / Modeler / Coder / Writer 全部实现
✅Code Interpreter 双形态local Jupyter + 云端 E2B/Daytona
✅LLM 多 ProviderOpenAI Chat / Responses / Anthropic 三套
✅Typst + LaTeX 双引擎17 套模板双版本
✅OpenAlex 文献检索替代原计划的 Tavily
✅9 步自动验收writing_check.sh + Typst/LaTeX 编译
✅取消信号贯穿asyncio.Event + asyncio.wait
✅Agent 记忆压缩的安全切割_find_safe_preserve_point 三段兜底
⚠️HIL 6 决策数据模型已实现,工作流未接
⚠️Evaluator Shadow Mode仅 Agent 基类里有 TODO
⚠️Feedback RerunWriter 后 Coder 的反馈注入未实现
⚠️RAG 知识库配置项存在,核心检索逻辑未实现
⚠️Fallback Hand Off基础重试机制已有,自动切备用模型未实现
⚠️英文支持(美赛)模板已就绪,Skill 描述未英化
❌多语言 R / MATLABREADME 列了未做
❌BenchmarkREADME 列了未做

最关键的 3 个未完成项:

  1. HIL 工作流集成——这是让 Agent 从”工具”走向”协作伙伴”的关键。数据模型已实现(schemas/A2A.py),只差把 pause/resume 接到 workflow 的关键节点上。
  2. RAG 知识库检索逻辑——docs/md/网络环境极差时的MathModelAgent配置过程.md 这类实战文档如果能被 RAG 检索到,会大幅降低用户冷启动成本。
  3. Fallback Hand Off 自动化——目前是手动配置,自动切备用模型的逻辑没写。

15. 与同类项目的对比#

数学建模类的自动化项目,我之前深拆过的有:

  • OpenScholar(AkariAsai/OpenScholar ↗)——学术研究问答,不是数学建模竞赛
  • Pi Agent Harness(earendil-works/pi ↗)——通用终端编码 Agent Harness,数学建模只是它的应用场景之一
  • DeepScientist——科研操作系统,一类相似场景
  • HyperFrames——HTML/CSS → MP4,完全不同领域

MathModelAgent 的独特定位:

维度OpenScholarPiMathModelAgent
定位ScholarQA通用 Coding Agent数学建模竞赛
SKILL/Harness自研自研 PiHarness+SKILL
多 Agent否单 Agent + 扩展4 Agent 串行
学术专用强(45M 论文)弱强(17 套模板)
桌面版否否有(Electron)
Code Interpreter否E2B 集成本地 Jupyter + E2B/Daytona
HIL否否数据模型已建

MathModelAgent 是”高度垂直场景 + Harness+SKILL 架构”的典型样本——它不需要做通用 Agent,只做数学建模,但做得很深。

MathModelAgent 的 WebUI:聊天界面 + 实时进度

16. 后记:为什么这件事值得做#

数学建模竞赛从 1992 年开始,30 多年了。它的形式——“3 天 3 夜,一队 3 人,一篇论文”——一直不变。不是因为它最合理,而是因为它是最容易组织的一种”团队限时高压测试”。

但 30 年里变了很多的事:

  • 1992 年的选手,要查文献只能去图书馆
  • 2025 年的选手,可以问 LLM”ARIMA 和 LSTM 在能源预测哪个好”
  • 2030 年的选手,可能只需要按一个按钮,Agent 就跑出了论文

jihe520/MathModelAgent 在做的是把按钮按下去之前的所有复杂度,封装成 SKILL。它不是要替代选手,它是要让选手把时间花在模型选择 + 创新思路 + 表达打磨上,而不是花在”装环境 + 写代码 + 排版论文”上。

最后作者在 README 写的两句话我特别喜欢:

感谢开源的 latex 模板,我在此基础上转化为 typst 模板

此 SKILL 是一个基础模板,你可以基于此构建更适合你自己的 MathModel SKILL

它把”工具”做成了”生态”——17 套模板是基础,你可以在它上面加你自己的领域知识、你自己学校的评分偏好、你自己积累的图表风格。

Coder Agent 视角:代码执行 + 反思纠正

Writer Agent 视角:写作手 + 文献引用

姊妹项目 sci-box:科研图表模板(SHAP / ROC / Taylor / 云雨图)


项目链接:github.com/jihe520/MathModelAgent ↗

姊妹项目:github.com/jihe520/sci-box ↗(科研图表 & 流程图 SKILL)

作者:jihe520(开源两年,MCM/ICM 出身)

在线体验:mathmodel.top/home ↗

如果你也在做数学建模、自动化 Agent、或者”SKILL 取代 Harness”这条路线,这个仓库值得 clone 下来仔细读一遍 backend/app/core/agents/agent.py 的 302 行——那是整个项目最浓缩的工程精华。

深入解析 MathModelAgent:从 SKILL 到 4-Agent 自动跑完一篇建模论文
https://wth2jhl.online/blog/mathmodel-agent-deep-dive/
Author 小吴同学
Published at September 13, 2026