Files
zhuangang/plan/agent-mini-project.md
T

536 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 转岗计划 · 第 11~14 周:多 Agent 项目(项目三 · 阶段 3 主交付)
> 所属阶段:第三段“Agent 主线”。总纲见同目录 `README.md`。前置:第 6 周 Python、第 7~8 周 RAG 项目、第 9~10 周盘点和案例卡。
## 目标
把在公司里见过的多 Agent 产品“削”成一个**没有业务外壳**、可以公开演示的内核,证明你“会造”,而不只是“会改”。主框架用 **LangGraph**(招聘里要求 LangChain / LangGraph 实际项目经验的很多),但先花几天**手写一遍循环**,把原理吃透,面试时既能讲框架也能讲原理。
做完后要能演示:
1. 提一个问题 → 协调者派活给两个专家 → 专家可以调用工具(包括通过 MCP 调用你第 7~8 周做的 RAG 知识库)→ 多轮讨论 → 达成共识或到达最大轮数后结束 → 生成可审计的报告。
2. 中途 `kill -9` 杀掉服务进程 → 重新启动 → 任务从最近的检查点接着跑完。
3. 浏览器通过 SSE 看进度;关掉页面任务照跑;重新打开能补齐漏掉的事件。
4. 协调者觉得信息不够时停下来问人(LangGraph `interrupt`),人回答后继续。
5. 记得用户的偏好和以前的结论(长期记忆),下次提问时用上。
6. 有一个小评测集,能用数字说明改提示词或流程之后是变好还是变差。
另外用 1~2 天体验 Dify 或 Coze(扣子),能讲清低代码平台和写代码各自适合什么场景。
项目名:`agent-mini`,放在 `zhuangang/agent-mini/`。
## 范围
| 必须有 | 禁止有(本阶段) |
| --- | --- |
| 至少 2 个专家(提案者 proposer、评审者 critic)+ 1 个协调者 coordinator | 完整任务中心、字典、巡检 |
| 明确的结束条件:评审通过(共识)或达到最大轮数,规则写在代码里 | 照搬公司 UI、提示词或代码 |
| 工具调用(Function / Tool Calling),至少一个工具来自自己写的 MCP 服务 | 依赖公司内网才能演示 |
| 状态持久化(LangGraph 检查点存 SQLite,可选 PostgreSQL)+ 重启续跑 | 没有说明的纯聊天 demo 页 |
| 编排跑在服务端后台;SSE 推送进度,断线可补齐 | 一次接太多框架(LangGraph 之外不再引入别的 Agent 框架) |
| 人机协作(中断问人)、长期记忆、评测集 | |
| 不需要真实模型密钥也能跑通测试和界面演示(假模型) | |
讨论的题目用通用例子,比如“一个 5 人创业团队做内部工具,该选什么技术栈”“给一个读书 App 设计会员规则”,不要用公司业务题目。
## 技术选型
| 用途 | 选择 |
| --- | --- |
| 语言和依赖 | Python 3.12 + uv |
| 手写阶段 | asyncio + httpx(复用 RAG 项目的模型客户端思路) |
| 主框架 | LangGraph(StateGraph、检查点、interrupt、Store),模型接入用 LangChain 的对话模型封装 |
| 工具 | Function Calling;MCP 服务用官方 Python SDK(或 FastMCP)写;Agent 侧用 LangChain 的 MCP 适配器加载工具 |
| 检查点 | SQLite 检查点(`langgraph-checkpoint-sqlite`),可选 PostgreSQL 检查点 |
| 业务表 | 自己的 `runs` / `events` 表(SQLite),存任务状态、租约、事件 |
| 服务 | FastAPI + SSE |
| 前端 | 极简 `web/index.html`,可选用 Vue / React 重写 |
| 质量 | pytest、ruff、pyright;评测脚本 |
> LangGraph、LangChain 和 MCP 相关库更新很快,包名、导入路径、推荐写法都可能变(比如 MCP 适配器最近就从独立包并入了 LangChain)。**动手前以官方文档当前版本为准**,本文写的是概念和要建的文件,具体 API 名称如有出入按文档改。
## 核心设计(先读懂,再动手)
### 两层状态:检查点 vs 业务表(重点 B)
- **LangGraph 检查点**:图的内部状态(每个节点执行完的快照),用 `thread_id` 区分,负责“从哪一步接着跑”。
- **自己的业务表 `runs` / `events`**:任务的对外状态(排队、运行、等人、完成、失败、取消)、谁在跑(租约)、给前端的事件流、最终报告。列表页、权限、计费、审计都查这里。
为什么要两层:检查点是框架的内部格式,不适合直接给业务查询;业务表是**对外的权威来源**。这正是你在工作中见过的“草稿 / 作业 / 任务状态谁说了算”的问题,在这里给出你自己的答案,并写进 README。
### 状态机(业务表 `runs.status`)
```text
queued ──(被某个进程领取)──> running ──> completed
^ │ ├──> failed(模型错误、输出无法解析等,带 error_code)
│ │ ├──> cancelled(用户取消)
│ │ └──> awaiting_input(图里调用了 interrupt)
└──(人回答 / 失败后重试 / 进程崩溃后租约过期)──┘
```
### 图的结构(LangGraph)
```text
START → load_memory → coordinator ─┬→ proposer ─┐
^ ├→ critic ─┤
│ ├→ ask_human─┤(interrupt)
└─────────┴────────────┘
coordinator ─→ summarize → save_memory → END
```
- 路由函数(条件边)里**先检查硬规则**:最近一次评审是 `VERDICT: APPROVE` → 去 `summarize`;轮数达到上限 → 去 `summarize`;否则按协调者的结构化决定走。
- 专家节点内部可以调用工具(工具调用循环可以用 LangGraph 预置的工具节点,也可以自己写一个小循环)。
### 防止双跑(重点 A)和续跑(重点 C)
- `runs` 表上有租约字段 `lease_owner`、`lease_until`,带条件的 `UPDATE` 领取任务,运行中定期续租;进程崩溃后租约过期,看门狗重新领取,用同一个 `thread_id` 从最近检查点继续。
- 节点内正在进行的模型调用在恢复后会**重做**(至少一次),已经完成的节点不会重做。工具如果有副作用(比如“创建待办”),要设计成幂等(带请求 ID),README 里写清楚。
---
## 最终项目结构
```
agent-mini/
├── README.md
├── pyproject.toml
├── .env.example # 默认 LLM_PROVIDER=fake
├── docker-compose.yml # agent-mini、mcp 服务;可连第 8 周的 rag-qa
├── Dockerfile
├── docs/
│ ├── architecture.md # 组件图、状态机、图结构、崩溃恢复时序图
│ ├── design-decisions.md # 关键取舍
│ ├── mapping.md # 与生产概念的映射表
│ ├── handwritten-vs-langgraph.md # 手写版和 LangGraph 版对照
│ └── talk.md # 3 分钟讲解稿
├── handwritten/ # 第 11 周:手写版(理解原理,保留作对照)
│ ├── llm.py # 最小模型客户端(支持 tools 参数)
│ ├── tool_loop.py # 手写工具调用循环
│ ├── multi_agent_loop.py # 手写协调者 + 两个专家循环
│ └── test_handwritten.py
├── mcp_servers/
│ ├── knowledge_server.py # MCP 服务:知识库检索(调 rag-qa)、读笔记
│ └── todo_server.py # MCP 服务(可选):创建待办(调项目一的接口,幂等)
├── src/agent_mini/
│ ├── __init__.py
│ ├── config.py
│ ├── errors.py # 错误码
│ ├── models.py # Decision、RunStatus、事件结构
│ ├── llm.py # 构造对话模型:真实 / 假模型
│ ├── tools/
│ │ ├── local_tools.py # 本地工具:计算、当前时间
│ │ └── mcp_tools.py # 通过 MCP 加载外部工具
│ ├── graph/
│ │ ├── state.py # 图的 State 定义(含 reducer)
│ │ ├── prompts.py # 各角色提示词
│ │ ├── nodes.py # coordinator / proposer / critic / ask_human / summarize
│ │ ├── routing.py # 条件边:硬规则 + 协调者决定
│ │ ├── memory.py # load_memory / save_memory 节点(长期记忆)
│ │ └── build.py # 组装并编译图
│ ├── store/
│ │ ├── schema.sql # runs / events 业务表
│ │ └── run_store.py # 业务表读写、租约
│ ├── runner.py # RunManager:后台运行、租约续期、看门狗、取消
│ ├── report.py # 由检查点状态生成可审计报告
│ ├── api/
│ │ ├── app.py
│ │ ├── routes_runs.py
│ │ └── sse.py
│ └── cli.py
├── evals/
│ ├── cases.jsonl # 评测用例
│ ├── checks.py # 规则检查 + 模型评分
│ ├── run_evals.py
│ └── reports/ # 每次评测的结果
├── web/index.html
├── scripts/demo_crash_resume.sh
└── tests/
├── conftest.py
├── test_routing.py
├── test_nodes.py
├── test_graph_resume.py
├── test_run_store.py
├── test_mcp_server.py
└── test_api_sse.py
```
## 业务表(`store/schema.sql`)
```sql
CREATE TABLE IF NOT EXISTS runs (
id TEXT PRIMARY KEY, -- 同时作为 LangGraph 的 thread_id
user_id TEXT NOT NULL DEFAULT 'demo',
question TEXT NOT NULL,
status TEXT NOT NULL, -- queued/running/awaiting_input/completed/failed/cancelled
model TEXT NOT NULL,
max_rounds INTEGER NOT NULL,
human_question TEXT,
final_answer TEXT,
end_reason TEXT,
error_code TEXT,
error_message TEXT,
lease_owner TEXT,
lease_until REAL,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL REFERENCES runs(id),
type TEXT NOT NULL, -- run_created/node_finished/tool_called/awaiting_input/input_received/run_completed/run_failed/run_cancelled
data TEXT NOT NULL, -- JSON
created_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_events_run_seq ON events(run_id, seq);
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON runs(status, lease_until);
```
## 接口设计
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/runs` | 创建任务 `{question, max_rounds?}`,立即返回 `{id, status: "queued"}` |
| GET | `/runs` / `/runs/{id}` | 列表 / 详情(状态 + 讨论记录) |
| GET | `/runs/{id}/events` | SSE,支持 `Last-Event-ID` 或 `?after=seq` 补发 |
| POST | `/runs/{id}/input` | 回答问题 `{answer}`,仅 `awaiting_input` 可用 |
| POST | `/runs/{id}/retry` | 失败后重试,可换模型 `{model?}` |
| POST | `/runs/{id}/cancel` | 取消 |
| GET | `/runs/{id}/report` | Markdown 报告 |
| GET | `/memories` | 查看当前用户的长期记忆(可删除) |
---
## 第 11 周 · M1:手写循环 + Tool Calling + MCP
本周不用 LangGraph,目的是搞懂原理。代码放在 `handwritten/` 和 `mcp_servers/`。
### 第 1 天:项目初始化和最小模型客户端
**学习内容**
- 复习:消息列表(system / user / assistant / tool)、温度、最大输出长度、流式输出。
- 错误码沿用 RAG 项目(超时、限流、额度、鉴权、输出异常),哪些重试、哪些不重试。
**要建的文件**
- `uv init --package agent-mini`;先只加 `httpx pydantic pydantic-settings`,开发依赖 `pytest pytest-asyncio ruff pyright`。
- `src/agent_mini/config.py`、`src/agent_mini/errors.py`、`.env.example`、`.gitignore`。
- `handwritten/llm.py`:`async def chat(messages, tools=None) -> AssistantMessage`,`AssistantMessage` 含 `content` 和 `tool_calls`(id、函数名、参数 JSON 字符串)。
### 第 2 天:手写工具调用循环(Function / Tool Calling)
**学习内容**
- 工具调用的完整过程:把工具的名字、说明、参数 JSON Schema 发给模型 → 模型返回“想调用哪个工具、参数是什么” → **你的代码**执行工具 → 把结果作为 `tool` 消息追加回去 → 再问模型 → 直到模型给出最终回答。模型本身从不直接执行任何东西。
- 安全和可靠性:参数要校验(pydantic);工具出错时把错误信息返回给模型,让它自己纠正,而不是整个流程崩溃;限制最大循环次数;有副作用的工具要幂等、要能审计。
**要建的文件**
- `handwritten/tool_loop.py`:
- `@dataclass class Tool`:`name`、`description`、`args_model: type[BaseModel]`、`func`。
- `def to_schema(tool) -> dict`:用 `args_model.model_json_schema()` 生成工具描述。
- 三个工具:`calculator(expression)`(只允许数字和四则运算,不能用 `eval` 执行任意代码)、`now()`、`search_knowledge(query)`(调用第 8 周 `rag-qa` 的 `/ask` 接口;没启动时返回错误信息)。
- `async def run_tool_loop(question, tools, max_steps=6) -> LoopResult`:记录每一步调用了什么工具、参数、结果、耗时。
- `handwritten/test_handwritten.py`:用假模型(按脚本返回 `tool_calls`)测:正常调用后得出答案;参数不合法时错误被返回给模型;超过最大步数时停止。
**完成标准**:用真实模型问“距离 2027 年元旦还有多少天,再乘以 3”,日志里能看到先调 `now` 再调 `calculator`。
### 第 3 天:手写多 Agent 循环
**学习内容**
- 多 Agent 的本质:几个“角色 = 系统提示词 + 能看到的上下文 + 可用工具”,加上一个决定“下一步谁来做、什么时候结束”的调度逻辑。
- 结束条件写在代码里,不只靠模型自觉。
**要建的文件**
- `handwritten/multi_agent_loop.py`(目标 150~250 行):
- 协调者输出 JSON 决定 `{"action": "dispatch|ask_human|finish", "target", "instruction", "reason"}`,用 pydantic 校验,格式错误时带上错误重试一次。
- 提案者、评审者两个角色;评审者最后一行必须是 `VERDICT: APPROVE` 或 `VERDICT: REVISE`。
- 循环:硬规则(评审通过 / 达到最大轮数)→ 协调者决定 → 执行 → 记录。
- 每一步把状态写到一个 JSON 文件(先体会“持久化 + 续跑”要解决什么问题:进程中途退出后,从文件里读出状态继续)。
- 测试补充到 `handwritten/test_handwritten.py`:评审第二次通过时提前结束;一直不通过时在最大轮数结束。
**完成标准**:能不看代码讲清这个循环的每一步;能说出“写 JSON 文件”这种持久化方式有什么问题(并发、原子性、查询),为后面用检查点和数据库做铺垫。
### 第 4 天:MCP 概念 + 写一个 MCP 服务
**学习内容**
- MCP(Model Context Protocol)是什么:让 AI 应用以统一的方式接入外部工具和数据的协议。角色分为宿主(比如 Cursor、Claude 桌面版)、客户端、服务端;服务端可以提供工具(tools)、资源(resources)、提示词模板(prompts)。
- 传输方式:本地进程用 stdio,远程服务用 HTTP(Streamable HTTP)。
- 和 Function Calling 的关系:Function Calling 是“模型怎么表达要调工具”;MCP 是“工具怎么被统一发现和调用”,写一次 MCP 服务,各种支持 MCP 的应用都能用。
**要建的文件**
- 加依赖:MCP 官方 Python SDK(`mcp`)或 FastMCP,按官方文档选一个。
- `mcp_servers/knowledge_server.py`:
- 工具 `search_knowledge(query: str, top_k: int = 5)`:调用 `rag-qa` 的检索接口,返回片段和出处。
- 工具 `list_documents()`。
- 资源(可选):`notes://cards` 列出你的案例卡标题(只读脱敏后的 `notes/cards/`)。
- 每个工具都写清楚说明和参数说明(模型靠这些判断什么时候调用)。
- `mcp_servers/todo_server.py`(可选):工具 `create_todo(board_id, title, request_id)`,调用项目一“团队任务看板”的接口;`request_id` 用来保证重复调用不会建出两条(体会“有副作用的工具要幂等”)。
- `tests/test_mcp_server.py`:直接调用工具函数,测试正常和 `rag-qa` 不可用时的错误返回。
**运行方式**:用官方调试工具 MCP Inspector(`npx @modelcontextprotocol/inspector` 加上启动你服务的命令)连上去,手动调用每个工具。
**完成标准**:在 Inspector 里能看到并调用你的工具。
### 第 5 天:在 Cursor 和自己的 Agent 里用 MCP
**要建的文件**
- 在 Cursor 的 MCP 配置里加上你的 `knowledge_server`,在 Cursor 对话里让它“查一下我的知识库里关于 X 的内容”,确认能调用。把配置示例写进 `docs/mcp-usage.md`(不要提交真实路径里的个人信息和密钥)。
- `src/agent_mini/tools/mcp_tools.py`:用 LangChain 的 MCP 适配器(当前版本的推荐方式以官方文档为准)把 MCP 工具加载成 LangChain 工具,下周给 LangGraph 用。
- `handwritten/tool_loop.py`:加一个选项,从 MCP 服务读取工具列表来替代本地的 `search_knowledge`,体会“工具可以动态发现”。
**完成标准**:同一个 MCP 服务,Cursor 和你自己的 Agent 都能用。
### 第 6~7 天:对照文档和 M1 验收
- `docs/handwritten-vs-langgraph.md`:先写“手写版”这半边:循环、状态、持久化、工具调用、结束条件分别是怎么做的,哪里最容易出错。下周学完 LangGraph 再补另一半。
- 余下时间补测试、整理代码,提交推送。
**M1 验收**
- [ ] 手写工具调用循环和多 Agent 循环都能跑,测试通过
- [ ] 能讲清 Function Calling 的完整过程,以及工具出错、参数不合法、死循环怎么处理
- [ ] 写了至少一个 MCP 服务,Inspector 和 Cursor 都能调用
- [ ] 能讲清 MCP 和 Function Calling 的关系
- [ ] 在总纲进度记录里勾掉第 11 周
---
## 第 12 周 · M2:用 LangGraph 实现多 Agent 编排
### 第 1 天:LangGraph 基础
**学习内容**(跟官方入门教程走,边看边在 `scratch/` 里敲)
- `StateGraph`:状态(State)、节点(node,普通函数,读状态、返回要更新的字段)、边、条件边、`START` / `END`。
- reducer:字段怎么合并(比如消息列表是追加而不是覆盖)。
- 编译图、运行(一次性 / 流式)、画出图结构(导出 mermaid)。
- 预置的工具调用节点 / 预置的 ReAct Agent:知道有,什么时候用。
**完成标准**:照着教程做出一个带条件边和工具调用的小图,能说出它和你上周手写版的对应关系。
### 第 2 天:设计 State 和路由
**要建的文件**
- 加依赖:`langgraph`、`langchain-core`、`langchain-openai`(或你用的模型服务对应的集成包),以官方文档为准。
- `src/agent_mini/models.py`:`Decision`(pydantic,同上周)、`RunStatus`。
- `src/agent_mini/graph/state.py`:
- `question`、`turns`(追加型 reducer,每条含角色、指令、内容、调用过的工具)、`round`、`max_rounds`、`last_verdict`、`decision`、`human_answers`、`final_answer`、`end_reason`、`memories`。
- `src/agent_mini/graph/routing.py`:
- `def route_after_coordinator(state) -> str`:按顺序检查:评审已通过 → `"summarize"`;`round >= max_rounds` → `"summarize"`;`decision.action` 是 `dispatch` → 目标专家;`ask_human` → `"ask_human"`;`finish` → `"summarize"`。
- `tests/test_routing.py`:每条规则至少一个用例(纯函数测试,不需要模型)。
### 第 3 天:节点和提示词
**要建的文件**
- `src/agent_mini/graph/prompts.py`:协调者、提案者、评审者、总结者的提示词,全部自己写。
- `src/agent_mini/llm.py`:`def build_chat_model(settings)`:真实模型(OpenAI 兼容接口,配置 `base_url`、超时、重试)或假模型(测试用:按角色返回预设回答;工具调用的假回答如果麻烦,测试只覆盖不调工具的路径)。
- `src/agent_mini/graph/nodes.py`:
- `coordinator`:用结构化输出得到 `Decision`(模型不支持时退回“提取 JSON + pydantic 校验 + 重试一次”)。
- `proposer`:可以调用工具(`search_knowledge` 来自 MCP、`calculator`、`now`),内部是一个有最大次数限制的工具调用循环。
- `critic`:输出评审意见和 `VERDICT`,解析后写入 `last_verdict`。
- `summarize`:生成最终结论,写 `end_reason`。
- 每个节点控制传给模型的上下文:只给问题、最近 N 条发言摘要和本次指令(上下文工程)。
- `tests/test_nodes.py`:用假模型测协调者输出格式错误时的重试、评审结论解析。
### 第 4 天:组装图,命令行跑通
**要建的文件**
- `src/agent_mini/graph/build.py`:`def build_graph(model, tools, checkpointer=None, store=None)`:加节点、边、条件边,编译。
- `src/agent_mini/cli.py`:
- `agent-mini graph`:导出图结构的 mermaid,贴进 `docs/architecture.md`。
- `agent-mini run "问题" --memory-only`:不带持久化,流式打印每个节点的更新(谁、做了什么、调了什么工具)。
**运行方式**:先启动 `rag-qa` 和 MCP 服务,再 `uv run agent-mini run "5 人创业团队做内部工具该选什么技术栈,结合我的知识库里的笔记"`。
### 第 5 天:真实模型试跑和调提示词
- 用真实模型跑 5 个不同题目,输出存到 `docs/samples/`(检查没有敏感信息)。
- 观察:协调者是否按格式输出、评审是否太容易通过、专家是否乱调工具、轮数和耗时。调整提示词,每次改动记一行到 `docs/prompt-changelog.md`(第 14 周评测时会用到)。
### 第 6~7 天:对照文档、测试和 M2 验收
- 补完 `docs/handwritten-vs-langgraph.md` 的另一半:手写版的每个部分在 LangGraph 里对应什么,框架帮你做了什么,又有哪些事(租约、业务状态、幂等)框架不管、还得自己做。
- 测试、ruff、pyright 通过,提交推送。
**M2 验收**
- [ ] LangGraph 版能跑完一次带工具调用的多轮讨论,结束条件正确
- [ ] 路由规则有单元测试
- [ ] 能讲清 State、节点、条件边、reducer,以及和手写版的对应关系
- [ ] 在总纲进度记录里勾掉第 12 周
---
## 第 13 周 · M3:持久化续跑 + 人机协作 + 后台作业 + SSE + 记忆
### 第 1 天:检查点和续跑
**学习内容**
- 检查点(checkpointer):每个节点执行完自动保存状态;同一个 `thread_id` 再次运行就从最近的检查点继续;还能查看历史状态。
- 用 SQLite 检查点(需要 `aiosqlite`),注意用完关闭连接,否则程序可能退不出去。官方说明 SQLite 检查点不建议用于生产,生产用 PostgreSQL 检查点——这一点写进 README 的“取舍”里。
**要建的文件**
- `build.py`:接上 SQLite 检查点。
- `tests/test_graph_resume.py`:用假模型(每次调用加一点延迟)运行,跑完 2 个节点后取消任务(模拟崩溃);再用同一个 `thread_id` 继续运行,断言最终完成、已完成的节点没有重复执行(统计假模型被调用的次数)。
**完成标准**:能讲清检查点保存了什么、恢复时从哪里继续、正在执行的节点恢复后会怎样。
### 第 2 天:业务表和后台作业
**要建的文件**
- `src/agent_mini/store/schema.sql`、`run_store.py`:
- `create_run`、`get_run`、`list_runs`、`set_status`、`append_event`、`list_events(run_id, after_seq)`。
- 租约:`try_acquire(run_id, worker_id, lease_seconds) -> bool`(带条件的 `UPDATE`,影响行数为 1 才算成功)、`renew_lease`、`release`、`find_resumable(now)`。
- `src/agent_mini/runner.py`:`class RunManager`:
- `submit(run_id)`:领取成功才启动后台任务,任务对象保存起来。
- `_drive(run_id)`:用 `thread_id = run_id` 流式运行图;每收到一个节点更新就写一条 `events`;定期续租;正常结束、中断、出错时更新 `runs` 并释放租约。
- `watchdog()`:定期找“该有人跑但没人跑”的任务重新提交。
- `cancel(run_id)`、`shutdown()`。
- `tests/test_run_store.py`:两个 worker 抢同一任务只有一个成功;租约过期后另一个能抢到。
### 第 3 天:人机协作(interrupt)和失败重试
**学习内容**
- LangGraph 的 `interrupt`:在节点里暂停并把问题交给调用方,状态由检查点保存;之后用“恢复命令”带上人的回答继续,`interrupt` 的返回值就是人的回答。
- 注意:恢复时这个节点会**从头重新执行**,所以 `interrupt` 之前不要放有副作用的操作。
- 什么时候必须停下来问人(重点 D):信息不足、要执行有副作用或高成本的操作、多次失败。
**要建的文件**
- `nodes.py`:`ask_human` 节点调用 `interrupt`,把问题写进状态。
- `runner.py`:检测到中断时 `runs.status = awaiting_input`,写 `human_question`,追加 `awaiting_input` 事件,释放租约。
- `POST /runs/{id}/input` 的处理逻辑(下面第 4 天接口里实现):写 `input_received` 事件,状态改回 `queued`,后台用恢复命令继续。
- 失败重试:`AgentError` 时 `runs.status = failed` 并写错误码;`POST /retry` 可以换模型后从检查点继续(对应工作中“额度不足 → 换模型恢复”的真实问题)。
- 测试:中断 → 回答 → 完成;模型报错 → 失败 → 换模型重试 → 完成。
### 第 4 天:FastAPI 接口和 SSE
**要建的文件**
- `src/agent_mini/api/app.py`:`lifespan` 里打开检查点和业务库、构建图、启动 `RunManager` 和看门狗;关闭时依次清理。
- `src/agent_mini/api/routes_runs.py`:接口表里除 SSE 外的接口。
- `src/agent_mini/api/sse.py`:从 `events` 表按 `seq` 补发,再等新事件(进程内通知加速,超时就轮询数据库兜底);15 秒无事件发心跳;任务到终态后发 `end` 并结束;客户端断开不影响任务。
- `src/agent_mini/report.py`:从检查点状态和 `events` 生成报告:问题、每一步谁做了什么、调用了哪些工具和结果摘要、结束原因、最终结论、使用的模型(重点 E:结果可审计)。
- `tests/test_api_sse.py`:创建 → 读 SSE 到结束;带 `Last-Event-ID` 只收到之后的事件;不存在的任务 404。
### 第 5 天:长期记忆(Memory)
**学习内容**
- 短期记忆:当前任务(线程)内的状态和对话,检查点已经管了。
- 长期记忆:跨任务记住的东西,比如用户偏好(“我们团队只用 Go 和 Vue”)、以前讨论得出的结论。LangGraph 提供 Store(按命名空间存取,可以配合向量做语义搜索)。
- 记忆要可查看、可删除;写什么进记忆要有规则,不能把所有对话都塞进去(成本、隐私、干扰)。
**要建的文件**
- `src/agent_mini/graph/memory.py`:
- `load_memory` 节点:按 `(user_id, "memories")` 命名空间取出与问题相关的若干条,放进状态,协调者和专家的提示词里引用。
- `save_memory` 节点:任务完成后,让模型从讨论中提炼 0~3 条“值得长期记住的事实或偏好”(结构化输出),写入 Store。
- Store 的持久化:本项目可以先用 SQLite 实现一个简单的 Store,或者使用官方提供的持久化 Store(以文档为准);在 `design-decisions.md` 里说明选择。
- `GET /memories`、`DELETE /memories/{key}`。
- 测试:第一次任务里说明偏好 → 第二次任务的提示词里出现这条记忆。
### 第 6 天:页面和崩溃演示
**要建的文件**
- `web/index.html`(原生 JS,不超过 250 行,可选之后用 Vue / React 重写):提问、时间线(每个节点、工具调用)、等待输入时出现输入框、失败时显示错误码和“换模型重试”、结论和报告链接、记忆列表、连接状态。
- `scripts/demo_crash_resume.sh`:用假模型加延迟启动服务 → 创建任务 → 等几秒 `kill -9` → 重启 → 轮询到完成 → 打印报告。
### 第 7 天:走查和 M3 验收
手动走查:正常跑完;中途关页再打开;中途停服务再启动;触发问人并回答;用错误的密钥制造失败后修好重试;第二次提问用上了记忆。
**M3 验收**
- [ ] 测试全部通过,包括续跑、租约、SSE、中断恢复
- [ ] 崩溃恢复演示脚本成功
- [ ] 6 个走查场景全部符合预期
- [ ] 能讲清:两层状态(检查点 vs 业务表)、为什么编排不能放在浏览器里、什么时候停下来问人、怎么防双跑、长期记忆写什么不写什么
- [ ] 在总纲进度记录里勾掉第 13 周
---
## 第 14 周 · M4:评测 + 文档 + 低代码平台体验
### 第 1 天:评测集
**学习内容**
- Agent 评测比 RAG 更难,因为过程有多步。从三个层面看:
- **结果**:最终结论是否回答了问题、是否符合约束。
- **过程**:有没有该调的工具没调、不该调的乱调;轮数是否合理;该问人时有没有问。
- **成本和稳定性**:耗时、token 数、失败率。
- 检查方式:能用规则判断的用规则(调了哪些工具、轮数、是否包含关键词);不能的用“模型按评分标准打分”,并抽查一部分人工复核,看两者是否一致。
**要建的文件**
- `evals/cases.jsonl`:20 条用例,每条包含 `id`、`question`、`expect`(例如 `must_call_tools`、`must_not_call_tools`、`should_ask_human`、`max_rounds`、`must_mention`)、`rubric`(给模型打分用的标准)。覆盖:普通问题、需要查知识库的问题、信息不足需要问人的问题、诱导乱调工具的问题。
- `evals/checks.py`:规则检查函数 + 模型评分函数(评分输出用结构化格式:分数 + 理由)。
- `evals/run_evals.py`:逐条运行(需要问人的用例自动给一个预设回答),汇总通过率、平均轮数、工具调用正确率、平均耗时和 token 数,结果写到 `evals/reports/<日期>.md`,记录当时的模型和提示词版本。
### 第 2 天:用评测驱动改进
- 跑基线,挑失败最多的一类问题,改提示词或路由规则,再跑,对比结果(数字以实际跑出来的为准)。改动和结果记到 `docs/prompt-changelog.md`。
- 用假模型做一个不需要密钥的小评测子集,放进测试,防止以后改代码把流程改坏(回归测试)。
### 第 3 天:文档
**要建的文件**
- `docs/architecture.md`:组件图、业务状态机(`stateDiagram-v2`)、LangGraph 图结构(第 12 周导出的)、崩溃恢复时序图(`sequenceDiagram`)。对照第 9 周画的公司系统脱敏图,写一段“我简化了什么”。
- `docs/mapping.md`(同时摘进 README,生产一栏只写通用概念,不写公司接口名、表名):
| 生产系统里的概念(通用叫法) | 本项目里的实现 | 对应重点 |
| --- | --- | --- |
| 席位 / 专家角色 | `proposer`、`critic` 节点 | A |
| 主持人派活 | `coordinator` 节点 + 条件边 | A |
| 共识 / 结束条件 | `VERDICT: APPROVE` + 最大轮数,写在 `routing.py` | A |
| 草稿(编排中间状态) | LangGraph 检查点(按 `thread_id`) | B |
| 任务状态(对外权威) | `runs` 表 | B |
| 后台作业 | `RunManager` + 租约 + 看门狗 | A / C |
| 流式输出 | `GET /runs/{id}/events`,按 `seq` 补发 | C |
| 意图澄清 / 等待用户输入 | `interrupt` → `awaiting_input` → `POST /input` | D |
| 换模型恢复 | 错误码 + `POST /retry {model}` | D |
| 过程报告 | `report.py` | E |
| 外部能力接入 | MCP 服务 `knowledge_server` | E |
- `docs/design-decisions.md`:每条写“选择 / 理由 / 代价”:两层状态;编排在后台;租约防双跑;节点内调用至少一次、副作用工具要幂等;结束条件写在代码里;SQLite 检查点只用于演示,生产换 PostgreSQL;长期记忆的写入规则;没做的事(鉴权、多实例下的实时通知、更完整的评测)。
### 第 4 天:README 和从零验证
- `README.md`:一句话介绍 + 动图;功能列表;**10 分钟快速开始**(假模型模式,不需要密钥);切换真实模型;连接 `rag-qa` 和 MCP;接口表;评测结果表;崩溃恢复演示;映射表;项目结构。
- `Dockerfile`、`docker-compose.yml`:`agent-mini` + MCP 服务,可选连接 `rag-qa`。
- 在新目录(最好换一台电脑或请朋友)`git clone` 后按 README 操作并计时,目标 10 分钟内跑通;卡住的地方都改进。
- 脱敏检查:`git grep` 搜公司名、内网域名、IP、密钥前缀。
### 第 5 天:讲解稿
- 录一段 2~3 分钟的演示:提问 → 工具调用 → 杀进程 → 重启续跑 → 问人 → 报告 → 评测结果。
- `docs/talk.md`:3 分钟讲解稿,“问题(为什么多 Agent 难做可靠)→ 设计(两层状态、后台作业、租约、中断、记忆)→ 演示 → 评测和取舍”,引用 1~2 张案例卡说明“这些设计来自我在工作中踩过的坑”。
### 第 6~7 天:Dify / Coze 体验(低代码平台)
**为什么**:招聘里经常出现 Dify、Coze、RAGFlow、FastGPT 这类平台。不需要精通,但要能说清楚它们能做什么、和写代码比各有什么优缺点,最好亲手搭过一个。
**做什么(二选一为主,另一个看看文档即可)**
- **Dify**(开源,可自己部署):在你的 Ubuntu 服务器上用官方的 Docker Compose 部署(注意服务器内存是否够用,以官方要求为准),或用官方云版。
- **Coze(扣子)**:直接用在线版。
**要搭的东西**
1. 知识库问答:把 `rag-qa` 用的同一批公开文档导入平台的知识库,做一个问答应用,用你第 8 周的评测集里挑 10 个问题对比效果。
2. 工作流:做一个带条件分支的工作流,包含一次 HTTP 请求节点调用你的 `agent-mini` 接口或 `rag-qa` 接口(平台支持 MCP 的话也可以接你的 MCP 服务)。
3. 导出应用配置(平台支持导出时),放到 `zhuangang/lowcode/`(去掉密钥)。
**要建的文件**
- `notes/lowcode-dify-coze.md`:
- 搭建步骤和截图。
- 和你自己的代码实现对比:开发速度、可控性(提示词、检索参数、状态、错误处理)、可观测性、部署和成本、团队里谁来维护。
- 结论:什么场景用平台(快速验证、业务人员自己搭、标准问答),什么场景写代码(复杂编排、强可靠性要求、深度集成业务系统)。
**M4 验收(也是阶段 3 出门条件)**
- [ ] 别人按 README 10 分钟内跑通(不需要真实模型密钥)
- [ ] 演示:启动 →(中途杀进程)→ 重启 → 最终结果可查
- [ ] 映射表至少 5 个概念对齐生产
- [ ] 有评测集和至少两次对比结果
- [ ] 架构图和 README 一致;测试、ruff、pyright 通过;脱敏检查完成
- [ ] Dify 或 Coze 至少搭过一个知识库应用和一个工作流,写了对比笔记
- [ ] 在总纲进度记录里勾掉第 14 周和“阶段 3”,填上“迷你项目路径:`zhuangang/agent-mini/`”
---
## 和案例卡的关系
第 10 周每张案例卡都有一栏“若重做的设计”。动手前把这些结论列出来,逐条对应到本项目的某个模块或某条设计取舍;对应不上的,在 `docs/design-decisions.md` 里说明为什么这次没做。面试时就能讲成:“我在工作中遇到 X 问题,根因是 Y,所以在自己的项目里用 Z 解决,这是代码和演示。”
## 可选扩展(第 15 周有余力再做,二选一)
- **Go 网关**:`zhuangang/agent-gateway/`,Gin 服务,复用项目一的 JWT 鉴权;`/api/runs/*` 反向代理到 Python 服务(SSE 要关闭缓冲);加用户级限流(Redis);转发时带上用户 ID,Python 端按用户隔离任务和记忆。讲点:“Go 做接入层,Python 做 Agent 编排”。
- **Vue / React 前端**:用你熟悉的框架重写 `web/index.html`,体现前端功底。