36 KiB
转岗计划 · 第 11~14 周:多 Agent 项目(项目三 · 阶段 3 主交付)
所属阶段:第三段“Agent 主线”。总纲见同目录
README.md。前置:第 6 周 Python、第 78 周 RAG 项目、第 910 周盘点和案例卡。
目标
把在公司里见过的多 Agent 产品“削”成一个没有业务外壳、可以公开演示的内核,证明你“会造”,而不只是“会改”。主框架用 LangGraph(招聘里要求 LangChain / LangGraph 实际项目经验的很多),但先花几天手写一遍循环,把原理吃透,面试时既能讲框架也能讲原理。
做完后要能演示:
- 提一个问题 → 协调者派活给两个专家 → 专家可以调用工具(包括通过 MCP 调用你第 7~8 周做的 RAG 知识库)→ 多轮讨论 → 达成共识或到达最大轮数后结束 → 生成可审计的报告。
- 中途
kill -9杀掉服务进程 → 重新启动 → 任务从最近的检查点接着跑完。 - 浏览器通过 SSE 看进度;关掉页面任务照跑;重新打开能补齐漏掉的事件。
- 协调者觉得信息不够时停下来问人(LangGraph
interrupt),人回答后继续。 - 记得用户的偏好和以前的结论(长期记忆),下次提问时用上。
- 有一个小评测集,能用数字说明改提示词或流程之后是变好还是变差。
另外用 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)
queued ──(被某个进程领取)──> running ──> completed
^ │ ├──> failed(模型错误、输出无法解析等,带 error_code)
│ │ ├──> cancelled(用户取消)
│ │ └──> awaiting_input(图里调用了 interrupt)
└──(人回答 / 失败后重试 / 进程崩溃后租约过期)──┘
图的结构(LangGraph)
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)
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 文件(先体会“持久化 + 续跑”要解决什么问题:进程中途退出后,从文件里读出状态继续)。
- 协调者输出 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(扣子):直接用在线版。
要搭的东西
- 知识库问答:把
rag-qa用的同一批公开文档导入平台的知识库,做一个问答应用,用你第 8 周的评测集里挑 10 个问题对比效果。 - 工作流:做一个带条件分支的工作流,包含一次 HTTP 请求节点调用你的
agent-mini接口或rag-qa接口(平台支持 MCP 的话也可以接你的 MCP 服务)。 - 导出应用配置(平台支持导出时),放到
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,体现前端功底。