计划调整为 16 周:以 Python/Agent 为主线,新增 RAG 项目、Redis/消息队列/Docker、LangGraph、MCP、记忆、评测和岗位要求对照

This commit is contained in:
Shikui Wang committed 2026-10-22 18:17:06 +08:00
1 parent f8a25cb765
commit 1e6315bb40
11 files changed
+1205 -629

No files matched your search

+249 -193
View File
@@ -1,4 +1,4 @@
# 转岗计划总纲:Go 全栈基础 + Python 多 Agent 应用
# 转岗计划总纲:Python AI 应用 / Agent 开发为主,Go 全栈为辅(16 周)
这是整个计划的入口文件。换了电脑或者隔一段时间回来,先看这份文件的“进度记录”,再打开当前所在周的计划文件继续做。
@@ -14,7 +14,7 @@
- 7 年 Web 前端(Vue / React),成人本科。
- AI 写代码越来越多,“只会把页面写出来”的价值在下降,需要往“能设计、能交付、能讲清原理”的方向走。
- 学历在大厂简历筛选里吃亏,所以主攻看作品和经验的岗位,用能跑起来的项目和真实案例说话。
- 第 3 节的招聘调研显示:AI Agent 岗位的主语言是 **Python**,Go 多数只是加分项。所以本计划以 **Python + AI 应用**为求职主线,Go 全栈作为基础和加分项。
### 起点(已经具备,不用从零学)
@@ -22,22 +22,22 @@
- 在公司里接触过真实的多 Agent 产品链路:意图识别 → 多角色研讨 → 总结 / 交付 / 过程报告。
- 踩过真实的生产问题:流式连接断开、模型额度不足、状态没有回写、数据库表结构和代码不一致、关掉页面后编排中断。
这些经历是转岗最大的本钱,后面的计划会把它们整理成“案例卡”和一个可以公开演示的迷你项目。
这些经历是转岗最大的本钱,后面的计划会把它们整理成“案例卡”,并做成可以公开演示的项目。
### 目标岗位
### 目标岗位(按优先级)
- 应用侧 Agent 工程师
- 大模型应用开发工程师
- 全栈工程师(前端 + Go 后端)
1. AI Agent 开发 / 大模型应用开发工程师(应用侧)
2. AI 全栈工程师(前端 + Python 或 Go 后端 + 大模型集成)
3. 全栈工程师(前端 + Go 后端)
### 终点(4 条必须同时满足,才算这条路径走完)
1. **能讲清**:一张架构图 + 三分钟口述“多 Agent 如何状态机化、状态存在哪、失败怎么办”。
2. **能做小**:一个**不带公司业务外壳**的迷你多 Agent 项目,本地可跑,覆盖编排 + 持久化 + 断点续跑(或写清降级策略)。
2. **能做小**:一个**不带公司业务外壳**的多 Agent 项目,本地可跑,覆盖编排 + 持久化 + 断点续跑(或写清降级策略)。
3. **能举证**:2 到 3 个“问题 → 根因 → 方案”的案例,优先来自自己在工作中遇到的真实问题(脱敏后)。
4. **能对标岗位**:简历和面试条目写得像“应用侧 Agent 工程师 / 全栈工程师”,而不是“改过公司某个页面”。
4. **能对标岗位**:简历和面试条目写得像“应用侧 Agent 工程师 / AI 全栈工程师”,而不是“改过公司某个页面”。
另外,第 1 个月结束时要有一个能演示的 Go 全栈项目(项目一:团队任务看板),作为全栈能力的证明。
另外还有两个作品:项目一“团队任务看板”(Go 全栈)和项目二“RAG 知识库问答”(Python)。
任意一条没满足,都不要提前宣布“学完了”。
@@ -45,8 +45,7 @@
- 不以模型预训练、微调为主业。
- 不把公司业务代码开源,也不整仓搬进作品集。
- 不深入运维巡检、内网穿透、多云部署,简历上最多一句带过。
- 本计划不再单独做“文档知识库问答(RAG)”项目,也不专门学 Redis、Nginx、HTTPS 部署。这些放到“完成后的下一步”里,不塞进本计划(见第 10 节)。
- 不深入 Kubernetes、微服务治理、运维巡检、多云部署(招聘里提到微服务时,能讲清概念和取舍即可)。
---
@@ -54,84 +53,146 @@
| 用途 | 选择 | 说明 |
| --- | --- | --- |
| 全栈后端语言 | Go | 第 1 个月主线。语法简单、风格统一,适合用 AI 辅助编写和审查 |
| Web 框架(Go) | Gin | 国内使用广,概念少 |
| 数据库 | PostgreSQL | 第 3 周开始用;Agent 项目默认用 SQLite,零依赖、方便别人跑起来,可选换成 PostgreSQL |
| 数据访问(Go) | sqlc + pgx | 写真 SQL,自动生成类型安全的 Go 代码,顺便练 SQL |
| 数据库迁移 | golang-migrate | 表结构变更全部用迁移文件管理,避免“库表和代码不一致” |
| 鉴权 | bcrypt + JWT | 第 4 周 |
| 前端 | React 或 Vue(你的强项) | 前后端分离,前端只负责调接口 |
| Agent 项目语言 | Python 3.12 | 第 8 周补课,第 9 到 11 周做项目。Agent 相关的框架、资料和岗位要求大多是 Python |
| Python 工具 | uv(管理环境和依赖)、ruff(格式化和检查)、pytest | |
| Agent 项目框架 | **自写 asyncio 编排循环**(默认) | 参考文档要求“LangGraph 或自写循环二选一,不要两个都深挖”。默认选自写,理由见下面 |
| Agent 服务 | FastAPI + SSE | 后台跑任务、前端通过 SSE 订阅进度 |
| 大模型调用 | httpx 调 OpenAI 兼容接口 + 一个假模型(FakeLLM) | 假模型让测试和演示不依赖密钥和网络 |
| AI 编程工具 | Cursor / Cursor CLI / Grok Build | 让 AI 写,你负责设计、审查、调试 |
**为什么默认自写 asyncio 循环,而不是 LangGraph**
- 面试要证明的是“懂编排、状态、续跑的原理”,自己写一遍循环、检查点和恢复逻辑,最能讲清楚。
- 代码量不大(核心几百行),出了问题容易调试。
- 公司里的 Agent 网关如果已经用了 LangGraph 一类的框架,你自写一遍之后再去读它,会更容易看懂它替你做了什么。
- 如果你在第 5 周盘点时更想用 LangGraph(比如目标公司明确要求),可以换,`agent-mini-project.md` 最后有一张“自写版模块 ↔ LangGraph 对应概念”的表。**只选一个,不要两个都做。**
| **求职主语言** | **Python 3.12** | 第 6 周开始,AI 相关岗位的主要要求 |
| Python Web | FastAPI | 招聘里 FastAPI、Flask 都常见,会一个另一个很快 |
| Python 工具 | uv、ruff、pyright、pytest | |
| RAG | 手写主流程 + LangChain 对照;向量库 Chroma(默认),了解 Milvus、pgvector | 招聘里点名的向量库主要是 Milvus 和 Chroma |
| Agent 框架 | **LangGraph**(主),先手写一遍循环理解原理 | 招聘普遍要求 LangChain / LangGraph 实际经验 |
| 工具与协议 | Function / Tool Calling、MCP | |
| 低代码平台 | Dify 或 Coze(体验 1~2 天) | 能讲清和写代码的取舍 |
| 加分语言 | Go + Gin | 第 1~5 周,全栈基础 |
| 数据库 | PostgreSQL(主学)+ MySQL 差异速查;SQLite(Agent 项目演示) | 招聘里 MySQL 更多,SQL 大部分通用 |
| Go 数据访问 | sqlc + pgx,golang-migrate 管理迁移 | |
| 缓存 / 队列 | Redis(缓存、限流、会话记忆)、Redis Streams(消息队列入门),了解 RabbitMQ 概念 | |
| 前端 | React 或 Vue + TypeScript(你的强项) | |
| 部署 | Docker、Docker Compose、Nginx、Linux 服务器、CI | |
| AI 编程工具 | Cursor / Cursor CLI / Grok Build | 让 AI 写,你负责设计、审查、调试;简历里也写上 |
---
## 3. 学习原则
## 3. 岗位要求对照
### 调研说明
- 来源:智联招聘,2026-10-22 检索“AI Agent开发”和“Go全栈”,共约 17 个岗位。BOSS 直聘需要登录,没有拿到数据。
- 样本不大,只能看出方向,不代表整个市场。下面只写这次实际看到的内容,没有额外统计数字。建议每过一个月自己再搜一次,更新这一节。
### 学历和经验
- 学历:约 95% 的岗位写的是“本科”;只有 1 个要求统招硕士,1 个标注统招本科;没看到要求 985 / 211 的。
- 经验:大多要求 3~5 年。
### AI Agent 开发岗位(按出现频率从高到低)
| 要求 | 本计划在哪一周覆盖 |
| --- | --- |
| Python + FastAPI / Flask | 第 6 周(Python、FastAPI),第 7~8、11~14 周项目里持续用 |
| RAG + 向量数据库(Milvus / Chroma) | 第 7~8 周 项目二(Chroma 实现,Milvus / pgvector 对比和可选实现) |
| LangChain、LangGraph(要求实际项目经验) | 第 8 周第 5 天 LangChain 重写 RAG;第 12~14 周 LangGraph 多 Agent 项目 |
| LlamaIndex;Dify / Coze / RAGFlow / FastGPT | 第 8 周了解 LlamaIndex;第 14 周第 6~7 天动手搭 Dify 或 Coze,其余看文档 |
| Function / Tool Calling、MCP | 第 11 周(手写工具调用循环、写 MCP 服务) |
| Prompt / Context Engineering、Agent 评测 | 第 7 周(提示词、上下文拼接)、第 8 周(RAG 评测集)、第 12 周(提示词迭代)、第 14 周(Agent 评测集) |
| 工作流编排、Memory、多 Agent(加分项) | 第 12 周(LangGraph 编排)、第 8 周(短期记忆)、第 13 周(长期记忆、持久化、人机协作) |
| Docker、Linux、Git | 第 5 周(Docker、Compose、部署、CI),第 8、14、15 周部署 AI 项目;Git 全程 |
| AI 编程工具(Cursor / Claude Code) | 全程使用;第 11 周把自己的 MCP 服务接进 Cursor;简历里写清工作流 |
| Go / Java(加分项) | 第 1~5 周 Go |
### Go 全栈岗位
| 要求 | 本计划在哪一周覆盖 |
| --- | --- |
| 后端语言多为“Java / Go / Node / Python 任选其一”,很少只要 Go | Go(第 1~5 周)+ Python(第 6 周起),两门都能拿出项目 |
| React / Vue + TypeScript | 已有强项;第 2、4、7 周项目里的前端 |
| MySQL、Redis(PostgreSQL 只出现过一次) | 第 3 周数据库(附 MySQL 差异和练习)、第 5 周 Redis |
| 微服务、消息队列 | 第 5 周消息队列入门(Redis Streams,了解 RabbitMQ);微服务只讲概念 |
| RESTful 接口 | 第 2、4 周 |
| Docker、CI/CD | 第 5 周 |
| “AI 全栈”:RAG / 大模型集成 | 第 7~8 周 项目二 |
| Gin、Kubernetes | 招聘里没有点名;Gin 作为学习用框架保留,Kubernetes 不学 |
### 对学习重点的影响
1. Python 是主语言,Go 是加分项;第 1~5 周的 Go 只学到“能做出一个带缓存、队列、部署的完整项目”为止。
2. RAG 和 LangChain / LangGraph 是硬要求,单独做成项目二和项目三。
3. 工具调用、MCP、评测、记忆这些“新要求”都要有能演示的代码,而不是只看过文章。
4. 数据库按 PostgreSQL 学,但要能说清和 MySQL 的差异。
---
## 4. 学习原则
1. **每一步都落到文件**:每天的任务都有明确要建的文件和完成标准;每周至少有一个能运行的提交或一页笔记。禁止“只看不写”。
2. **AI 写,你审**:代码可以让 AI 生成,但每个文件你都要能讲清楚每一行在做什么,面试时这是最大的区分点。
3. **边上班边学**:工作日每天 1.5 到 2 小时,周末多投入一些。不建议裸辞。
4. **代码和计划都进 Git**:全部推到 `zhuangang` 仓库,换设备直接接着做。
5. **只砍不加**:进度落后时砍范围,不加新内容;新想法记到第 10 节“完成后的下一步”。
6. **脱敏**:公司代码、内网地址、真实任务数据、密钥、客户名,一律不进仓库。工作里的经验只以“机制 + 脱敏案例”的形式记录(详见第 8 节)。
3. **先手写,再用框架**:RAG 和 Agent 都是先手写一遍核心流程,再用 LangChain / LangGraph 重写,两种都能讲。
4. **先有评测,再谈优化**:RAG 和 Agent 都有自己的小评测集,优化要拿数字说话(数字必须是自己跑出来的)。
5. **边上班边学**:工作日每天 1.5 到 2 小时,周末多投入一些。不建议裸辞。
6. **代码和计划都进 Git**:全部推到 `zhuangang` 仓库,换设备直接接着做。
7. **只砍不加**:进度落后时砍范围,不加新内容;新想法记到第 11 节“完成后的下一步”。
8. **脱敏**:公司代码、内网地址、真实任务数据、密钥、客户名,一律不进仓库(详见第 9 节)。
---
## 4. 十二周总路线
## 5. 十六周总路线
```text
第 1 个月 Go 全栈基础(第 1~4 周) → 项目一:团队任务看板
第 2 个月 阶段0 盘点 + 阶段1 读通画图(第 5 周)
阶段2 五个重点 A~E + 案例卡(第 6~7 周)
Python 补课(第 8 周)
第 3 个月 阶段3 Python 迷你多 Agent 项目(第 9~11 周)★主交付
阶段4 包装与终点验收 + 投递(第 12 周)
第一段 Go 全栈基础(第 1~5 周) → 项目一:团队任务看板(Go + Redis + 消息队列 + Docker)
第二段 Python 与 AI 应用(第 6~8 周) → 项目二:RAG 知识库问答(FastAPI + Chroma + Redis)
第三段 Agent 主线(第 9~14 周)
阶段0 盘点 + 阶段1 读通画图(第 9 周)
阶段2 五个重点 A~E + 案例卡(第 10 周)
阶段3 多 Agent 项目(第 11~14 周)★主交付 → 项目三
第四段 包装与求职(第 15~16 周) 阶段4 + 终点验收 + 投递
```
精力不够时可以拉长到 14 到 16 周,但顺序不要变,也不要跳过验收。
### 第 1 个月:Go 全栈基础
| 周 | 主题 | 产出 | 计划文件 |
| --- | --- | --- | --- |
| 第 1 周 | Go 基础语法 | 命令行任务清单 `todo` | `week01-go-basics.md` |
| 第 2 周 | Gin 写 HTTP 接口 + 接前端 | 任务清单 Web 版(内存存储) | `week02-gin-api.md` |
| 第 3 周 | PostgreSQL、SQL、迁移、sqlc、事务 | 任务清单改用数据库存储 | `week03-postgresql.md` |
| 第 4 周 | 注册登录、JWT、权限 | **项目一:团队任务看板**(多用户、成员权限) | `week04-auth-project1.md` |
| 1 | Go 基础语法 | 命令行任务清单 `todo` | `week01-go-basics.md` |
| 2 | Gin 写 HTTP 接口 + 接前端 | 任务清单 Web 版 | `week02-gin-api.md` |
| 3 | PostgreSQL、SQL、迁移、sqlc、事务 + MySQL 差异 | 任务清单改用数据库 | `week03-postgresql.md` |
| 4 | 项目一(上):注册登录、JWT、权限、前端 | 团队任务看板功能主体 | `week04-auth-project1.md` |
| 5 | 项目一(下):Redis 缓存限流、消息队列入门、Docker 部署、CI | 项目一部署上线 | `week05-project1-redis-docker.md` |
| 6 | Python 速成(asyncio、pydantic、httpx、FastAPI、SSE) | `py-learning/` | `week06-python-basics.md` |
| 7 | 项目二(上):手写 RAG 主流程 + 接口 + 前端 | RAG 问答可用 | `rag-project.md` |
| 8 | 项目二(下):评测、混合检索、记忆、Redis、LangChain 对照、部署 | RAG 项目完成 | `rag-project.md` |
| 9 | 阶段 0 盘点 + 阶段 1 读通与画图 | `notes/00`、`notes/01` | 本文第 6 节 |
| 10 | 阶段 2:五个重点 A~E + 案例卡 | `notes/cards/` | 本文第 6 节 |
| 11 | 项目三 M1:手写循环 + Tool Calling + MCP | 手写版 + MCP 服务 | `agent-mini-project.md` |
| 12 | 项目三 M2:LangGraph 多 Agent 编排 | LangGraph 版跑通 | `agent-mini-project.md` |
| 13 | 项目三 M3:检查点续跑、interrupt、后台作业、SSE、长期记忆 | 可演示崩溃恢复 | `agent-mini-project.md` |
| 14 | 项目三 M4:评测集、文档、映射表 + Dify / Coze 体验 | 项目三完成 | `agent-mini-project.md` |
| 15 | 阶段 4(上):部署作品、整理作品集、简历、口述稿 | `career/` | 本文第 6 节 |
| 16 | 阶段 4(下):面试准备、终点验收、投递 | 开始投递 | 本文第 6 节 |
月底验收:
- [ ] 能独立用 Go + Gin + PostgreSQL 写出带鉴权的 REST 接口
- [ ] 项目一本地 `docker compose up` + `go run .` + `npm run dev` 就能跑起来,README 写清楚
- [ ] 能讲清楚:接口隔离存储层、事务、索引、JWT 的优缺点
精力不够时可以再拉长,但顺序不要变,也不要跳过验收。
### 第 2 个月:盘点、读通、深挖、补 Python
### 时间不够时怎么砍(按顺序,越靠前越先砍)
#### 第 5 周:阶段 0 盘点(前 2~3 天)+ 阶段 1 读通与画图(后 4~5 天)
1. 所有标了“可选”的内容(Milvus / pgvector 实现、Go 网关、`todo_server`、前端重写)。
2. 第 5 周的 CI 配置(保留本地脚本)、第 3 周 MySQL 小练习(保留差异速查表)。
3. 第 1 周第 6 天的并发练习压缩为半天(第 6 周 asyncio 会再讲并发)。
4. **不能砍**:RAG 评测、LangGraph 项目的续跑和 SSE、MCP 服务、案例卡、Agent 评测集。
---
## 6. 第 9~10 周和第 15~16 周的具体安排
(第 1~8 周和第 11~14 周见各自的计划文件,这里是没有单独文件的几周。)
### 第 9 周:阶段 0 盘点(前 2 天)+ 阶段 1 读通与画图(后 5 天)
**阶段 0 做什么**
1. 写下一句话目标岗位(例:应用侧 Agent 工程师 / 大模型应用开发)。
1. 写下一句话目标岗位(参考第 3 节)。
2. 列出自己在公司亲自改过、排查过的模块(只写通用名字,例如“前端多 Agent 研讨模块”“Agent 网关的作业接口”)。
3. 按第 5 节 A~E 五个重点,填一张“已会 / 模糊 / 不会”三列对照表。
4. 确认迷你项目技术栈:Python + 自写 asyncio 循环(默认)或 LangGraph,二选一。
3. 按第 7 节 A~E 五个重点,填一张“已会 / 模糊 / 不会”三列对照表。
4. 确认项目三技术栈:Python + LangGraph(主)+ 先手写循环(已定)。
文件:`notes/00-inventory.md`
阶段 0 验收:
- [ ] 一句话目标岗位已写下
- [ ] A~E 的“已会 / 模糊 / 不会”三列填完
- [ ] 迷你项目技术栈已选定,写进 `notes/00-inventory.md`
- [ ] 项目三技术栈已确认
**阶段 1 做什么**(重点 A 编排 + B 权威源,先建立全局地图,再钻细节)
@@ -141,90 +202,60 @@
| --- | --- | --- |
| P0 | 前端的多 Agent 功能:意图、多席研讨、草稿、作业 | 几个步骤怎么串起来,谁发起 SSE |
| P0 | 前端的会话与交付模块 | 会话状态和交付状态分别存在哪 |
| P0 | Agent 网关里多 Agent、意图、作业、草稿相关的接口 | 服务端是“单轮调用”还是“后台作业” |
| P0 | Agent 网关里多 Agent、意图、作业、草稿相关的接口 | 服务端是“单轮调用”还是“后台作业”;用了什么框架、检查点存在哪 |
| P1 | 前端对 Agent 网关的接口契约清单 | 前后端到底约定了哪些接口 |
| P2 | 团队里关于“后台挂起 / 后台作业”的设计文档(如果有) | 理想架构和现状差在哪 |
产出:
1. **一张图**(mermaid):用户 → 前端 → Agent 网关 → 草稿 / 作业 / 业务库。文件 `notes/01-architecture.md`
2. **一张表**:关键接口(初始化、运行并流式返回、草稿、作业、会话)各自职责。文件 `notes/01-api-table.md`(接口只写职责,不写真实路径和内网地址)
2. **一张表**:关键接口(初始化、运行并流式返回、草稿、作业、会话)各自职责。文件 `notes/01-api-table.md`(只写职责,不写真实路径和内网地址)
阶段 1 验收:
- [ ] 不看代码能口述多 Agent 功能的主流程
- [ ] 图上标出:**关掉浏览器时,流程断在哪一层**
- [ ] 指出至少一处“状态双写或权威来源不清”的位置
#### 第 6 周:阶段 2(上)重点 A 编排 + C 流式
### 第 10 周:阶段 2 五个重点深挖 + 案例卡
| 重点 | 必做实验(二选一即可) | 产出 |
| --- | --- | --- |
| A 多 Agent 编排 | 跟一次完整的多 Agent 流程(或用联调桩),记录派活顺序和结束(共识)条件 | `notes/cards/A-orchestration.md` |
| C 流式与长任务 | 故意断网或关页面,记录“单轮继续”和“整个循环停掉”的区别 | `notes/cards/C-streaming.md` |
每个重点做一个实验(二选一即可),产出一张案例卡(模板见第 8 节):
周末顺手补完阶段 1 没画完的部分。
| 天 | 重点 | 必做实验 | 产出 |
| --- | --- | --- | --- |
| 1~2 | A 多 Agent 编排 | 跟一次完整的多 Agent 流程(或用联调桩),记录派活顺序和结束(共识)条件 | `notes/cards/A-orchestration.md` |
| 2~3 | C 流式与长任务 | 故意断网或关页面,记录“单轮继续”和“整个循环停掉”的区别 | `notes/cards/C-streaming.md` |
| 4 | B 状态与权威源 | 对比草稿数据和任务 / 过程报告,找出哪些没有回写 | `notes/cards/B-source-of-truth.md` |
| 5 | D 人机协作 | 走一遍“需要澄清”或“换模型失败 → 恢复” | `notes/cards/D-human-in-loop.md` |
| 6 | E 与业务系统集成 | 跟一次过程报告 / 任务状态:LLM 结果怎样变成业务记录 | `notes/cards/E-integration.md`(可以和 B 合并,但五个重点都要覆盖) |
| 7 | 回顾 | 把每张卡的“若重做的设计”汇总成清单,作为项目三的设计输入 | `notes/cards/README.md` |
#### 第 7 周:阶段 2(下)重点 B 权威源 + D 人机协作 + E 业务集成
| 重点 | 必做实验 | 产出 |
| --- | --- | --- |
| B 状态与权威源 | 对比草稿数据和任务 / 过程报告,找出哪些没有回写 | `notes/cards/B-source-of-truth.md` |
| D 人机协作 | 走一遍“需要澄清”或“换模型失败 → 恢复” | `notes/cards/D-human-in-loop.md` |
| E 与业务系统集成 | 跟一次过程报告 / 任务状态:LLM 结果怎样变成业务记录 | `notes/cards/E-integration.md`(可以和 B 合并成一张,但五个重点都要覆盖) |
一周写五张卡比较紧,写不完的在第 11~13 周每周五补一张,但第 14 周前必须写完。
阶段 2 验收:
- [ ] A~E 每个重点至少一张案例卡(模板见第 6 节)
- [ ] 每张卡都写了“若重做,我会怎样设计”,这些结论会直接变成第 9~11 周迷你项目的设计依据
- [ ] A~E 每个重点至少一张案例卡
- [ ] 每张卡都写了“若重做,我会怎样设计”
#### 第 8 周:Python 补课(给 JS 开发者的速成)
### 第 15 周:阶段 4(上)作品上线和简历
计划文件:`week08-python-basics.md`
1. **部署**:项目二、项目三用 Docker Compose 部署到你的服务器;有域名的话配上 HTTPS(Nginx + 证书),没有就保证内网可访问并录好演示视频。
2. **作品集整理**:把 `task-board/`、`rag-qa/`、`agent-mini/` 各自同步到公开的 GitHub 仓库(再做一遍脱敏检查),README 顶部放演示动图。
3. **简历**(`career/resume.md`),按第 10 节的建议写。
4. **口述稿**(`career/pitch.md`):3 分钟架构 + 1 分钟“踩过的最大坑”,计时练习,总长不超过 4 分钟,覆盖 A / B / C 中至少两项。
5. 有余力再做 `agent-mini-project.md` 末尾的可选扩展(Go 网关或前端重写)。
内容:基础语法对照 JS、类型标注(typing)、asyncio、pydantic、httpx + pytest、FastAPI 基础和 SSE。
产出:`py-learning/` 练习目录,最后一天跑通一个能推送 SSE 的 FastAPI 小服务。
### 第 16 周:阶段 4(下)面试准备、终点验收、投递
这一周只学 Python,不开 Agent 项目,也不深挖新案例(戒律:同一周不同时开新框架和新功能)。
### 第 3 个月:迷你多 Agent 项目 + 包装求职
#### 第 9~11 周:阶段 3 迷你复现项目(主交付)★
计划文件:`agent-mini-project.md`
| 周 | 里程碑 | 产出 |
| --- | --- | --- |
| 第 9 周 | M1 单轮 LLM 调用封装(超时、可读的错误码)+ M2 多轮编排循环 | 命令行能跑完一次“协调者 + 两个专家”的多轮讨论 |
| 第 10 周 | M3 状态持久化 + 进程重启后续跑 + FastAPI 后台作业 + SSE | 杀掉进程再启动,任务能接着跑完,浏览器断开重连能补齐事件 |
| 第 11 周 | M4 README、架构图、和生产概念的映射表;可选:Go 网关或极简前端 | 别人按 README 10 分钟内跑通 |
范围(来自参考文档):
| 必须有 | 本阶段禁止 |
| --- | --- |
| 至少 2 个专家角色 + 1 个协调者 | 完整任务中心、字典、巡检 |
| 明确的结束条件(达成共识或达到最大轮数) | 照搬公司 UI |
| 状态持久化(默认 SQLite,可换 PostgreSQL) | 依赖公司内网才能演示 |
| 进程重启后能续跑;编排跑在后台,不依赖浏览器 | 没有任何说明的纯聊天 demo 页 |
| SSE 推送进度,断线可重连补齐 | |
阶段 3 验收:
- [ ] 别人按 README 10 分钟内跑通(不需要真实模型密钥,用 FakeLLM 也能演示)
- [ ] 演示:启动 →(中途杀进程)→ 重启 → 最终结果可查
- [ ] 映射表至少 5 个概念对齐生产(角色 / 派活 / 共识 / 草稿 / 作业,或等价概念)
#### 第 12 周:阶段 4 包装与终点验收 + 投递
做什么:
1. 简历三条 Agent 相关经历(STAR 写法):编排可靠性、状态权威、业务落地各一条,现象和结果写具体(数字必须真实,没有就写现象,不要编)。另加一条 Go 全栈项目一。文件 `career/resume.md`
2. 面试口述稿:3 分钟架构 + 1 分钟“踩过的最大坑”。计时练习,总长不超过 4 分钟。文件 `career/pitch.md`
3. Go 和数据库基础复习:goroutine / channel、切片、接口、错误处理、索引、事务。文件 `career/interview-notes.md`
4. 对照第 1 节“终点”四条和第 12 节终点验收清单打勾;缺哪条补哪条,**不开新需求**。
5. 开始投递:中小公司、创业团队、AI 应用团队的 Agent 应用 / 大模型应用 / 全栈岗位。
1. 面试笔记 `career/interview-notes.md`,每个主题写“一句话答案 + 我的项目里怎么用的”:
- Python:asyncio、类型标注、pydantic、FastAPI。
- RAG:切分、向量检索、混合检索、重排、评测指标、拒答。
- Agent:工具调用、MCP、LangGraph 的状态 / 检查点 / 中断、记忆、多 Agent 编排、评测、常见失败和处理。
- 后端:数据库索引和事务、MySQL 与 PostgreSQL 差异、Redis 缓存问题、消息队列的幂等、Docker。
- Go(加分项):goroutine / channel、接口、错误处理。
2. 对照第 1 节“终点”四条和第 13 节终点验收清单打勾;缺哪条补哪条,**不开新需求**。
3. 开始投递(策略见第 10 节),每次面试后把被问到的问题补进面试笔记。
---
## 5. 五个学习重点(始终抓住这 5 条)
转岗面试和第 2、3 个月的自学,优先加深这五条,其他功能当背景。
## 7. 五个学习重点(始终抓住这 5 条)
| # | 重点 | 在工作中的抓手(通用叫法) | 学完应能回答的一句话 |
| --- | --- | --- | --- |
@@ -234,11 +265,11 @@
| D | 人机协作 | 意图澄清、等待用户输入、模型切换 | “Agent 什么时候必须停下来问人” |
| E | 与业务系统集成 | 过程报告、任务状态、鉴权隔离 | “LLM 的结果怎样落到可审计的业务对象上” |
迷你项目里每个重点都有对应的实现(见 `agent-mini-project.md` 的映射表)。
项目三里每个重点都有对应的实现(见 `agent-mini-project.md` 的映射表)。
---
## 6. 案例卡模板(直接复制)
## 8. 案例卡模板(直接复制)
每张卡一个文件,放在 `notes/cards/`。仓库里只放脱敏版本。
@@ -264,72 +295,86 @@
---
## 7. 每周节奏和戒律
## 9. 每周节奏、戒律和脱敏
| 日 | 做什么 |
| --- | --- |
| 周一 | 定本周重点(第 2、3 个月只选第 5 节中的一条,或一个里程碑) |
| 周一 | 定本周重点(第 9 周以后只选第 7 节中的一条,或一个里程碑) |
| 周二~周四 | 写代码 / 做实验 / 写案例卡 |
| 周五 | 验收:本周有没有“可展示的产出”;没有的话,周末只补交付,不学新东西 |
| 周日 | 30 分钟回顾:对照终点清单调整下一周范围(只砍不加),更新进度记录并 `git push` |
第 1 个月按每周计划文件里的“第 1 天~第 7 天”走,周五和周日的检查照做。
有计划文件的周按文件里的“第 1 天~第 7 天”走,周五和周日的检查照做。
**戒律**
- 同一周不同时开“新框架 + 新业务功能 + 新论文”。
- 公司需求和学习冲突时:学习这边只保“案例卡或迷你项目提交”,不保阅读量。
- 概念类资料每个重点最多读 1 篇(比如工具调用、结构化输出、人在回路),读完必须写进案例卡。
- 公司需求和学习冲突时:学习这边只保“案例卡或项目提交”,不保阅读量。
- 概念类资料每个重点最多读 1 篇,读完必须写进案例卡或项目文档。
- 不拿“无状态的聊天界面教程”当主线,最多用来对比“为什么不够”。
---
## 8. 脱敏和保密
**脱敏和保密**
- 仓库里**不放**:公司代码片段、内网地址和端口、真实接口路径、真实任务 / 客户数据、密钥、截图里的业务内容。
- 案例卡只写机制:“草稿状态没有回写到任务表”可以写;具体表名、接口路径、字段名不写。
- 需要记私有细节时,写在 `notes/private/`,并把它加进 `.gitignore`,只留在公司电脑上。
- 迷你项目必须是自己从零写的“无业务外壳”版本,角色和题目都用通用例子,不能复制公司代码或提示词。
- 每次 `git push` 前跑一遍 `git diff --cached`,看一眼有没有不该提交的内容。
- 私有细节写在 `notes/private/`(已加入 `.gitignore`),只留在公司电脑上。
- 项目二、三的文档素材、题目、提示词都用公开或自己写的内容,不复制公司的。
- 每次 `git push` 前看一眼 `git diff --cached`。
---
## 9. 仓库结构(`zhuangang`)
## 10. 简历和投递建议
**学历**
- 成人本科满足这次调研里绝大多数岗位的“本科”要求。简历上如实写“本科”和学校、专业、时间,不需要主动标注学习形式,但**也不要写成统招**;面试被问到时如实回答。
- 标了“统招本科”“统招硕士”的岗位直接跳过,不浪费时间。
- 没看到要求 985 / 211 的岗位,所以竞争点在项目和经验上。
**经验**
- 招聘多要求 3~5 年。你有 7 年工作经验,只是方向是前端:简历里把公司多 Agent 产品的经历写在最前面(你做了什么、解决了什么问题),这是“AI 应用经验”,不只是“前端经验”。
**项目(简历的重心)**
- 项目三(多 Agent)放第一,项目二(RAG)第二,项目一(Go 全栈)第三。
- 每个项目写:解决什么问题 → 关键设计(用岗位里的关键词:LangGraph、RAG、Chroma、MCP、Tool Calling、评测、Redis、Docker)→ 可验证的结果(评测数字、演示链接、仓库地址)。数字必须是你自己跑出来的,没有就写现象,不要编。
- 公司经历按 STAR 写三条:编排可靠性、状态权威、业务落地,内容来自案例卡(脱敏)。
- 技能栏顺序:Python / FastAPI → LangChain / LangGraph / RAG / MCP → 前端(Vue / React / TS)→ Go → 数据库和中间件 → Docker / Linux / Git → AI 编程工具。
**投递**
- 主投:AI Agent 开发、大模型应用开发、AI 全栈;中小公司、创业团队、AI 应用团队。
- 兼投:全栈(前端 + Go / Python)。
- 每个月按第 3 节的方法重新搜一次岗位要求,更新对照表,调整简历关键词。
---
## 11. 仓库结构(`zhuangang`)和换设备
```
zhuangang/
├── .gitignore # 至少包含 notes/private/、.env、*.db、node_modules/、.venv/
├── plan/ # 本计划文件夹(就是这些 .md 文件)
├── .gitignore # 至少包含 notes/private/、.env、*.db、data/、node_modules/、.venv/
├── plan/ # 本计划文件夹
│ ├── README.md
│ ├── week01-go-basics.md
│ ├── week02-gin-api.md
│ ├── week03-postgresql.md
│ ├── week04-auth-project1.md
│ ├── week08-python-basics.md
│ ├── week05-project1-redis-docker.md
│ ├── week06-python-basics.md
│ ├── rag-project.md
│ └── agent-mini-project.md
├── go-learning/ # 第 1 周语法练习,含第 7 天的 todo 命令行项目
├── todo-api/ # 第 2~3 周:任务清单 Web 版(Gin + PostgreSQL + 前端)
│ ├── backend/
│ └── frontend/
├── task-board/ # 第 4 周:项目一 团队任务看板
│ ├── backend/
│ └── frontend/
├── notes/ # 第 5~7 周:盘点、架构图、案例卡(全部脱敏)
│ ├── 00-inventory.md
│ ├── 01-architecture.md
│ ├── 01-api-table.md
├── go-learning/ # 第 1 周(含 todo 命令行项目)
├── todo-api/ # 第 2~3 周
├── task-board/ # 第 4~5 周:项目一
├── py-learning/ # 第 6 周
├── rag-qa/ # 第 7~8 周:项目二
├── notes/ # 第 9~10 周:盘点、架构图、案例卡(脱敏);低代码体验笔记
│ ├── cards/
│ └── private/ # 不进仓库(.gitignore)
├── py-learning/ # 第 8 周 Python 练习
├── agent-mini/ # 第 9~11 周:迷你多 Agent 项目(主交付)
└── career/ # 第 12 周:简历、口述稿、面试笔记
│ └── private/ # 不进仓库
├── agent-mini/ # 第 11~14 周:项目三(含 MCP 服务、评测集)
├── lowcode/ # 第 14 周:Dify / Coze 导出的配置(去掉密钥)
└── career/ # 第 15~16 周:简历、口述稿、面试笔记
```
以后整理作品集时,可以把 `task-board/` 和 `agent-mini/` 单独同步到公开的 GitHub 仓库(再检查一遍脱敏)。
---
## 10. 换设备后如何继续
**换设备后如何继续**
1. 新设备安装:Git、Go、Node.js、Docker、Python 3.12 和 uv、编辑器(VS Code 或 Cursor)。
2. 配好能访问 `git.gozeros.com` 的 SSH 密钥,然后克隆仓库:
@@ -341,52 +386,58 @@ zhuangang/
3. 打开 `plan/README.md`,在“进度记录”里找到上次做到哪周哪天。
4. 打开对应周的计划文件,从下一天继续。
5. 需要数据库的项目:在项目的 `backend/` 目录 `docker compose up -d` 启动 PostgreSQL,再按该项目 README 跑迁移。
6. Python 项目:进入 `agent-mini/`,运行 `uv sync` 安装依赖,复制 `.env.example` 为 `.env`。
7. 每天学完:更新进度记录 → `git add` → `git commit` → `git push`。
5. Go 项目:在项目目录 `docker compose -f docker-compose.dev.yml up -d` 启动数据库和 Redis,再按项目 README 跑迁移。Python 项目:进入项目目录 `uv sync`,复制 `.env.example` 为 `.env`。
6. 每天学完:更新进度记录 → `git add` → `git commit` → `git push`。
**完成后的下一步(不属于本计划,完成终点验收后再选一个开新路径)**
- 评测集:给 Agent 输出做自动评测。
- 工具调用规范:函数调用、MCP。
- 可观测性:链路追踪、日志、成本统计。
- RAG + pgvector 知识库、Redis 缓存、Docker + Nginx + HTTPS 正式部署。
- 可观测性:链路追踪、日志、成本统计(如 LangSmith、Langfuse 一类工具)。
- 更完整的评测体系和线上反馈闭环。
- Java / Spring 或 Go 微服务方向的深入。
- 模型微调入门。
---
## 11. 进度记录
## 12. 进度记录
### 按周
第 1~4 周和第 8 周按天打勾(☐ 改成 ☑)。第 5~7 周和第 9~12 周按“周一~周日”的节奏,周五验收时打勾。
每天学完把对应的 ☐ 改成 ☑。
| 周 | 主题 | 计划文件 | 每天 | 计划完成日 | 实际完成日 | 产出路径 | 周验收 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | Go 基础语法 | week01-go-basics.md | ☐☐☐☐☐☐☐ | | | go-learning/ | ☐ |
| 2 | Gin 接口 + 前端 | week02-gin-api.md | ☐☐☐☐☐☐☐ | | | todo-api/ | ☐ |
| 3 | PostgreSQL | week03-postgresql.md | ☐☐☐☐☐☐☐ | | | todo-api/ | ☐ |
| 4 | 鉴权 + 项目一 | week04-auth-project1.md | ☐☐☐☐☐☐☐ | | | task-board/ | ☐ |
| 5 | 阶段0 盘点 + 阶段1 画图 | README 第 4 节 | ☐☐☐☐☐☐☐ | | | notes/00、01 | ☐ |
| 6 | 阶段2 A 编排 + C 流式 | README 第 4 节 | ☐☐☐☐☐☐☐ | | | notes/cards/ | ☐ |
| 7 | 阶段2 B + D + E | README 第 4 节 | ☐☐☐☐☐☐☐ | | | notes/cards/ | ☐ |
| 8 | Python 补课 | week08-python-basics.md | ☐☐☐☐☐☐☐ | | | py-learning/ | ☐ |
| 9 | 阶段3 M1 + M2 | agent-mini-project.md | ☐☐☐☐☐☐☐ | | | agent-mini/ | ☐ |
| 10 | 阶段3 M3 持久化 + 续跑 + SSE | agent-mini-project.md | ☐☐☐☐☐☐☐ | | | agent-mini/ | ☐ |
| 11 | 阶段3 M4 文档 + 映射表 | agent-mini-project.md | ☐☐☐☐☐☐☐ | | | agent-mini/ | ☐ |
| 12 | 阶段4 包装 + 终点验收 + 投递 | README 第 4 节 | ☐☐☐☐☐☐☐ | | | career/ | ☐ |
| 3 | PostgreSQL + MySQL 差异 | week03-postgresql.md | ☐☐☐☐☐☐☐ | | | todo-api/ | ☐ |
| 4 | 项目一(上)鉴权权限 | week04-auth-project1.md | ☐☐☐☐☐☐☐ | | | task-board/ | ☐ |
| 5 | 项目一(下)Redis、队列、Docker | week05-project1-redis-docker.md | ☐☐☐☐☐☐☐ | | | task-board/ | ☐ |
| 6 | Python 速成 | week06-python-basics.md | ☐☐☐☐☐☐☐ | | | py-learning/ | ☐ |
| 7 | 项目二(上)手写 RAG | rag-project.md | ☐☐☐☐☐☐☐ | | | rag-qa/ | ☐ |
| 8 | 项目二(下)评测、优化、部署 | rag-project.md | ☐☐☐☐☐☐☐ | | | rag-qa/ | ☐ |
| 9 | 阶段0 盘点 + 阶段1 画图 | README 第 6 节 | ☐☐☐☐☐☐☐ | | | notes/00、01 | ☐ |
| 10 | 阶段2 案例卡 A~E | README 第 6 节 | ☐☐☐☐☐☐☐ | | | notes/cards/ | ☐ |
| 11 | 项目三 M1 手写 + 工具 + MCP | agent-mini-project.md | ☐☐☐☐☐☐☐ | | | agent-mini/ | ☐ |
| 12 | 项目三 M2 LangGraph 编排 | agent-mini-project.md | ☐☐☐☐☐☐☐ | | | agent-mini/ | ☐ |
| 13 | 项目三 M3 续跑、中断、SSE、记忆 | agent-mini-project.md | ☐☐☐☐☐☐☐ | | | agent-mini/ | ☐ |
| 14 | 项目三 M4 评测、文档 + Dify/Coze | agent-mini-project.md | ☐☐☐☐☐☐☐ | | | agent-mini/、lowcode/ | ☐ |
| 15 | 阶段4(上)部署、作品集、简历 | README 第 6 节 | ☐☐☐☐☐☐☐ | | | career/ | ☐ |
| 16 | 阶段4(下)面试准备、验收、投递 | README 第 6 节 | ☐☐☐☐☐☐☐ | | | career/ | ☐ |
### 按阶段(来自参考文档)
### 按阶段
| 阶段 | 计划完成日 | 实际完成日 | 产出路径 | 验收 |
| --- | --- | --- | --- | --- |
| 第 1 个月 Go 全栈基础 | | | task-board/ | ☐ |
| 第一段 Go 全栈基础(项目一) | | | task-board/ | ☐ |
| Python 速成 | | | py-learning/ | ☐ |
| 项目二 RAG | | | rag-qa/ | ☐ |
| 0 盘点 | | | notes/00-inventory.md | ☐ |
| 1 读通与画图 | | | notes/01-*.md | ☐ |
| 2 案例卡 | | | notes/cards/ | ☐ |
| Python 补课 | | | py-learning/ | ☐ |
| 3 迷你项目 | | | agent-mini/ | ☐ |
| 3 项目三(多 Agent) | | | agent-mini/ | ☐ |
| 4 终点 | | | career/ | ☐ |
**项目三路径**:(阶段 3 完成后填写)
### 卡住和收获(每天一句)
| 日期 | 周/天 | 一句话收获或卡住的地方 |
@@ -395,23 +446,28 @@ zhuangang/
---
## 12. 终点验收清单(全部勾选才算完结)
## 13. 终点验收清单(全部勾选才算完结)
**全栈能力**
- [ ] 项目一(团队任务看板)能本地跑起来,README 清楚
- [ ] 能讲清 goroutine、channel、切片底层、接口、错误处理
- [ ] 能讲清表结构设计、索引、事务、迁移、JWT 鉴权
**AI 应用能力**
- [ ] 项目二:RAG 问答可演示,有评测集和对比结果,README 清楚
- [ ] 项目三:LangGraph 多 Agent 可演示:启动 → 中途杀进程 → 重启续跑 → 结果可查
- [ ] 写过 MCP 服务,能讲清 Tool Calling 和 MCP
- [ ] 项目三有评测集,至少两次对比结果
- [ ] 搭过 Dify 或 Coze 应用,能讲清和写代码的取舍
- [ ] 架构图最新版已归档,和项目三 README 一致
**Agent 能力(参考文档的终点)**
- [ ] 架构图最新版已归档,和迷你项目 README 里的一致
- [ ] 迷你项目可演示:启动 → 中途杀进程 → 重启续跑 → 结果可查
**全栈能力(加分项)**
- [ ] 项目一部署过,能讲清分层、鉴权、缓存、限流、消息队列、部署
- [ ] 能讲清 MySQL 和 PostgreSQL 的主要差异
**参考文档的终点**
- [ ] 至少 2 张案例卡可以对外讲(已脱敏)
- [ ] 口述稿计时不超过 4 分钟,覆盖 A / B / C 中至少两项
- [ ] 第 1 节“终点”四条全部满足
**求职**
- [ ] 简历更新完成(Agent 三条 + 全栈一条),没有编造的数字
- [ ] 简历按第 10 节更新完成,没有编造的数字,学历如实
- [ ] 已开始投递
- [ ] 写下一句“下一步如果继续学”:评测集 / 工具调用规范 / 可观测性(三选一),作为新路径,不塞进本计划
- [ ] 写下一句“下一步如果继续学”(第 11 节里选一个),作为新路径,不塞进本计划
**完结动作**:把本文件开头的状态改成“已完成(YYYY-MM-DD)”,提交并推送。本路径正式结束。
+357 -415
View File
@@ -1,18 +1,21 @@
# 转岗计划 · 第 9~11 周:Python 迷你多 Agent 项目(阶段 3 主交付)
# 转岗计划 · 第 11~14 周:多 Agent 项目(项目三 · 阶段 3 主交付)
> 所属阶段:第 3 个月,阶段 3。总纲见同目录 `README.md`,前置补课见 `week08-python-basics.md`。
> 所属阶段:第三段“Agent 主线”。总纲见同目录 `README.md`。前置:第 6 周 Python、第 7~8 周 RAG 项目、第 9~10 周盘点和案例卡。
## 目标
把在公司里见过的多 Agent 产品“削”成一个**没有业务外壳**、可以公开演示的内核,证明你“会造”,而不只是“会改”。
把在公司里见过的多 Agent 产品“削”成一个**没有业务外壳**、可以公开演示的内核,证明你“会造”,而不只是“会改”。主框架用 **LangGraph**(招聘里要求 LangChain / LangGraph 实际项目经验的很多),但先花几天**手写一遍循环**,把原理吃透,面试时既能讲框架也能讲原理。
做完后要能演示:
1. 提一个问题 → 协调者派活给两个专家 → 多轮讨论 → 达成共识或到达最大轮数后结束 → 生成可审计的报告。
2. 中途 `kill -9` 杀掉服务进程 → 重新启动 → 任务从上一步接着跑完,已完成的步骤不重复。
1. 提一个问题 → 协调者派活给两个专家 → 专家可以调用工具(包括通过 MCP 调用你第 7~8 周做的 RAG 知识库)→ 多轮讨论 → 达成共识或到达最大轮数后结束 → 生成可审计的报告。
2. 中途 `kill -9` 杀掉服务进程 → 重新启动 → 任务从最近的检查点接着跑完。
3. 浏览器通过 SSE 看进度;关掉页面任务照跑;重新打开能补齐漏掉的事件。
4. 协调者觉得信息不够时停下来问人,人回答后继续。
5. 模型调用失败(超时、限流、额度、鉴权)时,任务进入失败状态并带上可读的错误码,可以换模型后继续。
4. 协调者觉得信息不够时停下来问人(LangGraph `interrupt`),人回答后继续。
5. 记得用户的偏好和以前的结论(长期记忆),下次提问时用上。
6. 有一个小评测集,能用数字说明改提示词或流程之后是变好还是变差。
另外用 1~2 天体验 Dify 或 Coze(扣子),能讲清低代码平台和写代码各自适合什么场景。
项目名:`agent-mini`,放在 `zhuangang/agent-mini/`。
@@ -21,11 +24,12 @@
| 必须有 | 禁止有(本阶段) |
| --- | --- |
| 至少 2 个专家(提案者 proposer、评审者 critic)+ 1 个协调者 coordinator | 完整任务中心、字典、巡检 |
| 明确的结束条件:评审通过(共识)或达到最大轮数 | 照搬公司 UI、提示词或代码 |
| 状态持久化:默认 SQLite(可选 PostgreSQL) | 依赖公司内网才能演示 |
| 编排跑在服务端后台,进程重启后能续跑 | 没有说明的纯聊天 demo 页 |
| SSE 推送进度,断线重连能补齐 | 同时再学 LangGraph(二选一,已选自写) |
| 不需要真实模型密钥也能演示(FakeLLM) | |
| 明确的结束条件:评审通过(共识)或达到最大轮数,规则写在代码里 | 照搬公司 UI、提示词或代码 |
| 工具调用(Function / Tool Calling),至少一个工具来自自己写的 MCP 服务 | 依赖公司内网才能演示 |
| 状态持久化(LangGraph 检查点存 SQLite,可选 PostgreSQL)+ 重启续跑 | 没有说明的纯聊天 demo 页 |
| 编排跑在服务端后台;SSE 推送进度,断线可补齐 | 一次接太多框架(LangGraph 之外不再引入别的 Agent 框架) |
| 人机协作(中断问人)、长期记忆、评测集 | |
| 不需要真实模型密钥也能跑通测试和界面演示(假模型) | |
讨论的题目用通用例子,比如“一个 5 人创业团队做内部工具,该选什么技术栈”“给一个读书 App 设计会员规则”,不要用公司业务题目。
@@ -34,51 +38,53 @@
| 用途 | 选择 |
| --- | --- |
| 语言和依赖 | Python 3.12 + uv |
| 编排 | 自写 asyncio 循环 |
| 模型调用 | httpx 调 OpenAI 兼容的 `/chat/completions` 接口;FakeLLM 用于测试和离线演示 |
| 数据校验 | pydantic v2、pydantic-settings |
| 存储 | SQLite(aiosqlite),通过 `RunStore` 协议隔离,可另写 PostgreSQL 实现 |
| 服务 | FastAPI + uvicorn,SSE 用 `StreamingResponse` 手写 |
| 前端 | 一个静态 `web/index.html`(原生 JS + `EventSource`),可选用你熟悉的 Vue / React 重写 |
| 质量 | pytest + pytest-asyncio、ruff、pyright |
| 手写阶段 | 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)
^ │ ├──> failed(模型错误、输出无法解析等,带 error_code)
│ │ ├──> cancelled(用户取消)
│ │ └──> awaiting_input(协调者要求问人)
│ │
│ │ └──> awaiting_input(图里调用了 interrupt)
└──(人回答 / 失败后重试 / 进程崩溃后租约过期)──┘
```
### 一“步”是什么
### 图的结构(LangGraph)
一步 = 协调者做一次决定 + 被派到的专家回答一次。每一步完成后,在**同一个数据库事务**里:
```text
START → load_memory → coordinator ─┬→ proposer ─┐
^ ├→ critic ─┤
│ ├→ ask_human─┤(interrupt)
└─────────┴────────────┘
coordinator ─→ summarize → save_memory → END
```
1. 插入这一步的发言记录 `turns`(主键 `run_id + step`,重复插入会被忽略,保证不会写出两条);
2. 更新 `runs.next_step`、`runs.round`、`runs.status`;
3. 追加事件 `events`(给 SSE 用)。
- 路由函数(条件边)里**先检查硬规则**:最近一次评审是 `VERDICT: APPROVE` → 去 `summarize`;轮数达到上限 → 去 `summarize`;否则按协调者的结构化决定走。
- 专家节点内部可以调用工具(工具调用循环可以用 LangGraph 预置的工具节点,也可以自己写一个小循环)。
进程在一步中间崩溃,这一步的模型调用会在恢复后**重做一次**(模型调用“至少一次”),但写进数据库的步骤“恰好一次”。这个取舍要写进 README。
### 防止双跑(重点 A)和续跑(重点 C)
### 结束条件写在代码里,不只靠模型自觉
- 评审者每次回答最后一行必须是 `VERDICT: APPROVE` 或 `VERDICT: REVISE`。最近一次评审是 `APPROVE` 时,代码直接让协调者收尾。
- 轮数达到 `max_rounds` 时,不再问协调者,直接收尾,并在报告里注明“未达成共识,因轮数上限结束”。
- 协调者输出格式错误:带上错误提示重试 1 次,还错就以 `LLM_BAD_OUTPUT` 失败。
### 防止同一个任务被跑两次(重点 A)
`runs` 表上有租约字段 `lease_owner`、`lease_until`。进程领取任务时用一条带条件的 `UPDATE`(只有租约为空、已过期或本来就是自己的才能领到),影响行数为 1 才算领取成功。运行中每步续租;进程崩溃后租约过期,看门狗会把任务重新领走。
### 权威数据在数据库(重点 B)
SSE 推送、内存里的通知都只是“加速”,任何时候以数据库里的 `runs` / `turns` / `events` 为准。SSE 断线重连就是按 `events.seq` 从数据库补发。
- `runs` 表上有租约字段 `lease_owner`、`lease_until`,带条件的 `UPDATE` 领取任务,运行中定期续租;进程崩溃后租约过期,看门狗重新领取,用同一个 `thread_id` 从最近检查点继续。
- 节点内正在进行的模型调用在恢复后会**重做**(至少一次),已经完成的节点不会重做。工具如果有副作用(比如“创建待办”),要设计成幂等(带请求 ID),README 里写清楚。
---
@@ -86,100 +92,94 @@ SSE 推送、内存里的通知都只是“加速”,任何时候以数据库
```
agent-mini/
├── README.md # 作品说明(M4)
├── pyproject.toml # 依赖、脚本入口 agent-mini、pytest/ruff 配置
├── .python-version
├── .env.example # 所有配置项和说明,默认 LLM_PROVIDER=fake
├── .gitignore # .env、data/、.venv/
├── README.md
├── pyproject.toml
├── .env.example # 默认 LLM_PROVIDER=fake
├── docker-compose.yml # agent-mini、mcp 服务;可连第 8 周的 rag-qa
├── Dockerfile
├── docs/
│ ├── architecture.md # 组件图、状态机、崩溃恢复时序图(mermaid)
│ ├── design-decisions.md # 关键取舍:为什么后台跑、租约、至少一次
│ └── mapping.md # 与生产概念的映射表(也摘进 README)
├── scripts/
│ └── demo_crash_resume.sh # 一键演示:启动 → 杀进程 → 重启 → 查看结果
├── web/
│ └── index.html # 极简页面:提问、看进度、回答问题
│ ├── 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 # Settings(pydantic-settings)
│ ├── errors.py # ErrorCode、AgentError
│ ├── models.py # Run、RunStatus、Turn、Decision、Event
│ ├── llm/
│ │ ├── __init__.py # build_llm(settings) 工厂函数
│ │ ├── base.py # ChatMessage、LLMResult、LLMClient 协议
│ │ ├── openai_compat.py # 真实调用:超时、重试、错误码映射
│ │ └── fake.py # 假模型:按脚本回复,可设延迟和故障
│ ├── roles/
│ │ ├── __init__.py
│ │ ├── base.py # Specialist 基类、构造提示词的公共函数
│ │ ├── proposer.py # 提案者
│ │ ├── critic.py # 评审者(输出 VERDICT)
│ │ └── coordinator.py # 协调者:decide()、summarize()
│ ├── parsing.py # 从模型输出里提取 JSON / VERDICT
│ ├── orchestrator.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/
│ │ ├── __init__.py
│ │ ├── base.py # RunStore 协议
│ │ ├── schema.sql # 建表语句
│ │ └── sqlite_store.py # SQLite 实现(事务、租约、事件)
│ ├── events.py # EventNotifier:进程内通知,加速 SSE
│ ├── runner.py # RunManager:后台任务、租约续期、看门狗恢复、取消
│ ├── report.py # 由 turns 生成可审计的 Markdown 报告
│ │ ├── schema.sql # runs / events 业务表
│ │ └── run_store.py # 业务表读写、租约
│ ├── runner.py # RunManager:后台运行、租约续期、看门狗、取消
│ ├── report.py # 由检查点状态生成可审计报告
│ ├── api/
│ │ ├── __init__.py
│ │ ├── app.py # create_app()、lifespan(启动看门狗)
│ │ ├── routes_runs.py # 任务相关接口
│ │ └── sse.py # GET /runs/{id}/events
│ └── cli.py # agent-mini ask / run / show / list / serve
│ │ ├── 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 # 临时数据库、FakeLLM、测试设置
├── test_llm_openai_compat.py
├── test_parsing.py
├── test_orchestrator.py
├── test_sqlite_store.py
├── test_runner_resume.py
├── 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`)
## 业务表(`store/schema.sql`)
```sql
CREATE TABLE IF NOT EXISTS runs (
id TEXT PRIMARY KEY, -- uuid
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, -- 本任务用的模型,可在重试时更换
round INTEGER NOT NULL DEFAULT 0,
model TEXT NOT NULL,
max_rounds INTEGER NOT NULL,
next_step INTEGER NOT NULL DEFAULT 1,
human_question TEXT, -- awaiting_input 时要问人的问题
human_question TEXT,
final_answer TEXT,
end_reason TEXT,
error_code TEXT,
error_message TEXT,
lease_owner TEXT,
lease_until REAL, -- Unix 时间戳
lease_until REAL,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS turns (
run_id TEXT NOT NULL REFERENCES runs(id),
step INTEGER NOT NULL,
role TEXT NOT NULL, -- proposer/critic/human/coordinator
instruction TEXT NOT NULL DEFAULT '', -- 协调者派活时说的话
content TEXT NOT NULL,
decision_json TEXT, -- 协调者这一步的决定原文(审计用)
model TEXT,
latency_ms INTEGER,
created_at REAL NOT NULL,
PRIMARY KEY (run_id, step)
);
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/decision/turn/awaiting_input/input_received/run_completed/run_failed/run_cancelled
data TEXT NOT NULL, -- JSON
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);
@@ -190,404 +190,346 @@ CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON runs(status, lease_until);
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/health` | 健康检查 |
| POST | `/runs` | 创建任务 `{question, max_rounds?}`,立即返回 `{id, status: "queued"}`,后台开始跑 |
| GET | `/runs` | 最近的任务列表 |
| GET | `/runs/{id}` | 任务状态 + 所有 turns |
| GET | `/runs/{id}/events` | SSE,支持 `Last-Event-ID` 请求头或 `?after=seq` 补发 |
| POST | `/runs/{id}/input` | 回答协调者的问题 `{answer}`,仅 `awaiting_input` 状态可用 |
| POST | `/runs/{id}/retry` | 失败后重试,可换模型 `{model?}`,仅 `failed` 状态可用 |
| 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 | `/` | 返回 `web/index.html` |
错误响应统一:`{"error": {"code": "RUN_NOT_FOUND", "message": "..."}}`。
| GET | `/runs/{id}/report` | Markdown 报告 |
| GET | `/memories` | 查看当前用户的长期记忆(可删除) |
---
## 第 9 周 · M1:单轮 LLM 调用封装(第 1~3 天)
## 第 11 周 · M1:手写循环 + Tool Calling + MCP
### 第 1 天:项目初始化、配置和错误码
本周不用 LangGraph,目的是搞懂原理。代码放在 `handwritten/` 和 `mcp_servers/`。
### 第 1 天:项目初始化和最小模型客户端
**学习内容**
- `src/` 布局的 Python 项目,`pyproject.toml` 里配置脚本入口。
- 配置集中管理,所有可调参数都能通过环境变量改。
- 为什么要有统一的错误码:日志、接口、前端提示都能按错误码处理,而不是靠解析错误文字。
- 复习:消息列表(system / user / assistant / tool)、温度、最大输出长度、流式输出。
- 错误码沿用 RAG 项目(超时、限流、额度、鉴权、输出异常),哪些重试、哪些不重试。
**要建的文件**
- `uv init --package agent-mini`,`uv python pin 3.12`;`uv add httpx pydantic pydantic-settings`;`uv add --dev pytest pytest-asyncio ruff pyright`。
- `pyproject.toml`:`[project.scripts] agent-mini = "agent_mini.cli:main"`;`[tool.pytest.ini_options] asyncio_mode = "auto"`。
- `.env.example`:`LLM_PROVIDER=fake`、`LLM_BASE_URL=`、`LLM_API_KEY=`、`LLM_MODEL=`、`LLM_TIMEOUT_S=60`、`LLM_MAX_RETRIES=2`、`DB_PATH=data/agent_mini.db`、`MAX_ROUNDS=6`、`LEASE_SECONDS=15`、`FAKE_DELAY_S=0`,每项上面一行注释。
- `.gitignore`:`.env`、`data/`、`.venv/`、`__pycache__/`。
- `src/agent_mini/config.py`:`class Settings(BaseSettings)`,字段同上;`llm_provider: Literal["fake", "openai"]`;`def get_settings() -> Settings`。
- `src/agent_mini/errors.py`:
- `class ErrorCode(StrEnum)`:`LLM_TIMEOUT`、`LLM_RATE_LIMITED`、`LLM_QUOTA_EXCEEDED`、`LLM_AUTH`、`LLM_BAD_REQUEST`、`LLM_UNAVAILABLE`、`LLM_BAD_OUTPUT`、`RUN_NOT_FOUND`、`RUN_INVALID_STATE`、`RUN_LEASE_LOST`。
- `class AgentError(Exception)`:`code: ErrorCode`、`message: str`、`retryable: bool`、`detail: dict | None`。
- 每个错误码配一句中文说明 `ERROR_HINTS: dict[ErrorCode, str]`,比如 `LLM_QUOTA_EXCEEDED: "模型额度不足,请更换模型或充值后重试"`。
- `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 字符串)。
**运行方式**:`uv run python -c "from agent_mini.config import get_settings; print(get_settings())"`
**完成标准**:改环境变量能改变配置;`uv run ruff check .` 通过。
### 第 2 天:OpenAI 兼容客户端
### 第 2 天:手写工具调用循环(Function / Tool Calling)
**学习内容**
- `/chat/completions` 请求和响应的结构(`messages`、`choices[0].message.content`、`usage`)。
- 超时分两种:连不上和等太久;整体超时用 `httpx.Timeout`。
- 错误分类:哪些重试(超时、429 限流、5xx、连接失败),哪些不重试(401/403 鉴权、400 参数、额度不足)。
- 指数退避:第 n 次重试前等 `base * 2**n` 秒,加一点随机抖动。
- 工具调用的完整过程:把工具的名字、说明、参数 JSON Schema 发给模型 → 模型返回“想调用哪个工具、参数是什么” → **你的代码**执行工具 → 把结果作为 `tool` 消息追加回去 → 再问模型 → 直到模型给出最终回答。模型本身从不直接执行任何东西。
- 安全和可靠性:参数要校验(pydantic);工具出错时把错误信息返回给模型,让它自己纠正,而不是整个流程崩溃;限制最大循环次数;有副作用的工具要幂等、要能审计。
**要建的文件**
- `src/agent_mini/llm/base.py`:
- `class ChatMessage(BaseModel)`:`role: Literal["system", "user", "assistant"]`、`content: str`。
- `class LLMResult(BaseModel)`:`content`、`model`、`prompt_tokens: int | None`、`completion_tokens: int | None`、`latency_ms: int`。
- `class LLMClient(Protocol)`:`async def chat(self, messages: list[ChatMessage], *, model: str | None = None, temperature: float = 0.3) -> LLMResult`;`async def aclose(self) -> None`。
- `src/agent_mini/llm/openai_compat.py`:
- `class OpenAICompatClient`:构造时创建 `httpx.AsyncClient(base_url, timeout, headers)`,可注入 `transport` 方便测试。
- `async def chat(...)`:发请求、计时、解析;按上面的分类转成 `AgentError`。429 时看响应体里有没有额度相关的错误类型(不同服务商字段不同,先按你实际用的服务商处理,并在注释里说明),有就是 `LLM_QUOTA_EXCEEDED`(不重试),否则 `LLM_RATE_LIMITED`(重试)。响应结构不对时 `LLM_BAD_OUTPUT`。
- `def _map_error(resp: httpx.Response) -> AgentError`:单独一个函数,方便测试。
- 日志里记录模型名、耗时、重试次数,**不记录 API Key 和完整提示词**。
- `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 天:FakeLLM、测试和 `ask` 命令
### 第 3 天:手写多 Agent 循环
**学习内容**
- 为什么需要假模型:测试要快、要稳定、不花钱;演示不依赖密钥和网络;可以故意制造故障。
- 多 Agent 的本质:几个“角色 = 系统提示词 + 能看到的上下文 + 可用工具”,加上一个决定“下一步谁来做、什么时候结束”的调度逻辑。
- 结束条件写在代码里,不只靠模型自觉。
**要建的文件**
- `src/agent_mini/llm/fake.py`:
- `class FakeLLM`:实现 `LLMClient` 协议。
- 根据 system 消息里的角色标记(比如 `[ROLE:critic]`)决定回复:协调者按“提案 → 评审 → 修改 → 评审”的顺序输出 JSON 决定;评审者第一次 `VERDICT: REVISE`、第二次 `VERDICT: APPROVE`。
- 参数 `delay_s`(每次调用等待多久,演示崩溃恢复用)、`fail_plan: dict[int, ErrorCode]`(第几次调用抛什么错误)、`calls: list` 记录每次调用,方便测试断言。
- `src/agent_mini/llm/__init__.py`:`def build_llm(settings) -> LLMClient`。
- `tests/test_llm_openai_compat.py`,用 `httpx.MockTransport`:
- 200 正常解析出 `content` 和 `usage`。
- 401 → `LLM_AUTH`,只请求了 1 次。
- 429 额度不足 → `LLM_QUOTA_EXCEEDED`,只请求了 1 次。
- 429 限流两次后 200 → 成功,请求了 3 次。
- 一直超时 → `LLM_TIMEOUT`,请求了 `1 + max_retries` 次。
- 响应里没有 `choices` → `LLM_BAD_OUTPUT`。
- 测试里把退避时间设为 0,别让测试变慢。
- `src/agent_mini/cli.py`:先实现 `agent-mini ask "问题"`:调一次模型,打印回复、耗时、token 数;出错时打印错误码和中文提示,退出码非 0。
- `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`:评审第二次通过时提前结束;一直不通过时在最大轮数结束。
**运行方式**:`uv run pytest -v`;`uv run agent-mini ask "你好"`(fake);配置好 `.env` 后用真实模型再试一次。
**完成标准**:能不看代码讲清这个循环的每一步;能说出“写 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 验收**
- [ ] 6 个客户端测试全部通过
- [ ] 真实模型调用一次成功;故意填错 Key,看到 `LLM_AUTH` 和中文提示
- [ ] 能说清楚错误码设计如何对应你在工作中遇到的“额度 / 鉴权失败”问题(对应案例卡 D 或 E)
- [ ] 手写工具调用循环和多 Agent 循环都能跑,测试通过
- [ ] 能讲清 Function Calling 的完整过程,以及工具出错、参数不合法、死循环怎么处理
- [ ] 写了至少一个 MCP 服务,Inspector 和 Cursor 都能调用
- [ ] 能讲清 MCP 和 Function Calling 的关系
- [ ] 在总纲进度记录里勾掉第 11 周
---
## 第 9 周 · M2:多轮编排循环(第 4~7 天)
## 第 12 周 · M2:用 LangGraph 实现多 Agent 编排
### 第 4 天:数据模型和输出解析
### 第 1 天:LangGraph 基础
**学习内容**(跟官方入门教程走,边看边在 `scratch/` 里敲)
- `StateGraph`:状态(State)、节点(node,普通函数,读状态、返回要更新的字段)、边、条件边、`START` / `END`。
- reducer:字段怎么合并(比如消息列表是追加而不是覆盖)。
- 编译图、运行(一次性 / 流式)、画出图结构(导出 mermaid)。
- 预置的工具调用节点 / 预置的 ReAct Agent:知道有,什么时候用。
**完成标准**:照着教程做出一个带条件边和工具调用的小图,能说出它和你上周手写版的对应关系。
### 第 2 天:设计 State 和路由
**要建的文件**
- `src/agent_mini/models.py`:
- `class RunStatus(StrEnum)`:6 个状态;`TERMINAL = {completed, failed, cancelled}`。
- `class Decision(BaseModel)`:`action: Literal["dispatch", "ask_human", "finish"]`、`target: Literal["proposer", "critic"] | None`、`instruction: str = ""`、`question_for_human: str | None = None`、`reason: str`;用 `model_validator` 保证 `dispatch` 必须有 `target`、`ask_human` 必须有 `question_for_human`。
- `class Turn(BaseModel)`:`step`、`role`、`instruction`、`content`、`decision_json`、`model`、`latency_ms`。
- `class Run(BaseModel)`:对应 `runs` 表的字段。
- `class Event(BaseModel)`:`seq`、`run_id`、`type`、`data: dict`。
- `src/agent_mini/parsing.py`:
- `def extract_json(text: str) -> str`:处理代码块包裹、前后多余文字(第 8 周第 5 天写过类似的)。
- `def parse_decision(text: str) -> Decision`:失败抛 `AgentError(LLM_BAD_OUTPUT)`。
- `def parse_verdict(text: str) -> Literal["APPROVE", "REVISE"] | None`:取最后一个 `VERDICT:` 行。
- `tests/test_parsing.py`:参数化测试,至少 8 种输入。
- 加依赖:`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`:每条规则至少一个用例(纯函数测试,不需要模型)。
### 第 5 天:角色和提示词
**学习内容**
- 角色 = system 提示词 + 能看到的上下文 + 输出格式要求。
- 上下文控制:专家只看到问题、之前的发言摘要和这次的指令;历史太长时只保留最近几步(先写死保留最近 6 步,在 README 里说明这是简化)。
- 结构化输出:协调者必须只输出 JSON,提示词里给出格式和例子。
### 第 3 天:节点和提示词
**要建的文件**
- `src/agent_mini/roles/base.py`:
- `def format_history(turns: list[Turn], limit: int = 6) -> str`
- `class Specialist`:`name`、`system_prompt`;`async def act(self, llm, question, turns, instruction, model) -> Turn`。
- `src/agent_mini/roles/proposer.py`:提案者,给出或修改方案,要求分点、具体。
- `src/agent_mini/roles/critic.py`:评审者,指出风险和遗漏,最后一行必须是 `VERDICT: APPROVE` 或 `VERDICT: REVISE`。
- `src/agent_mini/roles/coordinator.py`:
- `async def decide(self, llm, question, turns, round, max_rounds, model) -> Decision`:解析失败时把错误信息附在消息末尾重试 1 次。
- `async def summarize(self, llm, question, turns, end_reason, model) -> str`:输出最终结论。
- 所有提示词放在各自文件顶部的常量里,带 `[ROLE:xxx]` 标记(FakeLLM 靠它识别角色)。提示词全部自己写,不复制公司的。
- `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`:用假模型测协调者输出格式错误时的重试、评审结论解析。
### 第 6 天:编排循环(内存版)
**学习内容**
- 把“做一步”和“循环”分开:`step()` 是纯逻辑,方便测试;循环只负责反复调用 `step()` 并检查结束条件。下周加持久化时只改循环,不改 `step()`。
### 第 4 天:组装图,命令行跑通
**要建的文件**
- `src/agent_mini/orchestrator.py`:
- `class StepOutcome(BaseModel)`:`decision: Decision`、`turn: Turn | None`、`new_status: RunStatus`、`end_reason: str | None`、`final_answer: str | None`。
- `class Orchestrator`:持有 `llm` 和三个角色。
- `async def step(self, run: Run, turns: list[Turn]) -> StepOutcome`,规则按顺序:
1. 最近一次评审是 `APPROVE` → 直接 `summarize`,`completed`,结束原因“评审通过”。
2. `run.round >= run.max_rounds` → 直接 `summarize`,`completed`,结束原因“达到最大轮数”。
3. 否则调 `coordinator.decide`:`dispatch` → 调对应专家,`round + 1`;`ask_human` → `awaiting_input`;`finish` → `summarize`,`completed`。
- `async def run_in_memory(self, question: str, max_rounds: int, on_event) -> Run`:内存里循环调用 `step()`,每步调用 `on_event` 回调。
- `cli.py`:新增 `agent-mini run "问题" --max-rounds 6 --memory`,每步打印“第几步 / 谁 / 指令 / 回答前 200 字”,最后打印结论。
- `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`:不带持久化,流式打印每个节点的更新(谁、做了什么、调了什么工具)。
**运行方式**:`uv run agent-mini run "5 人创业团队做内部工具该选什么技术栈" --memory`
**运行方式**:先启动 `rag-qa` 和 MCP 服务,再 `uv run agent-mini run "5 人创业团队做内部工具该选什么技术栈,结合我的知识库里的笔记"`。
### 第 7 天:编排测试和真实模型试跑
### 第 5 天:真实模型试跑和调提示词
**要建的文件**
- `tests/test_orchestrator.py`(全部用 FakeLLM):
- 评审第二次通过 → 正常结束,步骤顺序是 proposer、critic、proposer、critic。
- 评审一直不通过 → 在 `max_rounds` 结束,结束原因是“达到最大轮数”。
- 协调者第一次输出乱码、第二次正常 → 成功;两次都乱码 → `LLM_BAD_OUTPUT`。
- 协调者要求问人 → 状态 `awaiting_input`,`question_for_human` 有值。
- 协调者派给不存在的角色 → 解析阶段就报错(`Literal` 校验)。
- 用真实模型跑 3 个不同题目,把输出存到 `docs/samples/`(检查没有敏感信息),观察协调者有没有不按格式输出、评审是否太容易通过,调整提示词。
- 用真实模型跑 5 个不同题目,输出存到 `docs/samples/`(检查没有敏感信息)。
- 观察:协调者是否按格式输出、评审是否太容易通过、专家是否乱调工具、轮数和耗时。调整提示词,每次改动记一行到 `docs/prompt-changelog.md`(第 14 周评测时会用到)。
### 第 6~7 天:对照文档、测试和 M2 验收
- 补完 `docs/handwritten-vs-langgraph.md` 的另一半:手写版的每个部分在 LangGraph 里对应什么,框架帮你做了什么,又有哪些事(租约、业务状态、幂等)框架不管、还得自己做。
- 测试、ruff、pyright 通过,提交推送。
**M2 验收**
- [ ] `uv run pytest` 全部通过
- [ ] 命令行能用 fake 和真实模型各跑完一次多轮讨论
- [ ] 能说清楚“结束条件为什么要写在代码里”(对应重点 A)
- [ ] 提交并推送;在总纲进度记录里勾掉第 9 周
- [ ] LangGraph 版能跑完一次带工具调用的多轮讨论,结束条件正确
- [ ] 路由规则有单元测试
- [ ] 能讲清 State、节点、条件边、reducer,以及和手写版的对应关系
- [ ] 在总纲进度记录里勾掉第 12 周
---
## 第 10 周 · M3:持久化 + 续跑 + 后台作业 + SSE
## 第 13 周 · M3:持久化续跑 + 人机协作 + 后台作业 + SSE + 记忆
### 第 1 天:SQLite 存储层
### 第 1 天:检查点和续跑
**学习内容**
- aiosqlite 用法;SQLite 开启 WAL 模式(`PRAGMA journal_mode=WAL`)让读写不互相卡住;`PRAGMA foreign_keys=ON`。
- 事务:`BEGIN IMMEDIATE ... COMMIT`。
- 幂等写入:`INSERT ... ON CONFLICT(run_id, step) DO NOTHING`(第 3 周学过 PostgreSQL 的写法,SQLite 语法几乎一样)。
- 检查点(checkpointer):每个节点执行完自动保存状态;同一个 `thread_id` 再次运行就从最近的检查点继续;还能查看历史状态。
- 用 SQLite 检查点(需要 `aiosqlite`),注意用完关闭连接,否则程序可能退不出去。官方说明 SQLite 检查点不建议用于生产,生产用 PostgreSQL 检查点——这一点写进 README 的“取舍”里。
**要建的文件**
- `uv add aiosqlite`
- `src/agent_mini/store/schema.sql`:见上面“数据库表”。
- `src/agent_mini/store/base.py`:`class RunStore(Protocol)`,方法:
- `create_run(question, max_rounds, model) -> Run`
- `get_run(run_id) -> Run`(找不到抛 `RUN_NOT_FOUND`)、`list_runs(limit) -> list[Run]`、`list_turns(run_id) -> list[Turn]`
- `commit_step(run_id, worker_id, outcome: StepOutcome, events: list[tuple[str, dict]]) -> None`:一个事务里写 turn、更新 run、追加事件;**先检查租约还是自己的**,不是就抛 `RUN_LEASE_LOST`,什么都不写。
- `try_acquire(run_id, worker_id, lease_seconds) -> bool`、`renew_lease(...) -> bool`、`release(run_id, worker_id)`
- `find_resumable(now) -> list[str]`:状态是 `queued` 或 `running` 且租约为空或已过期的任务。
- `set_status(run_id, status, **fields)`、`append_event(run_id, type, data) -> int`、`list_events(run_id, after_seq) -> list[Event]`
- `src/agent_mini/store/sqlite_store.py`:`class SqliteRunStore`,`async def open(path)` 时执行建表和 PRAGMA。
- `tests/test_sqlite_store.py`(用 `tmp_path` 下的临时数据库):
- 同一步 `commit_step` 两次,`turns` 只有一行。
- 两个 worker 抢同一个任务,只有一个 `try_acquire` 成功;租约过期后另一个能抢到。
- 租约被别人抢走后,原 worker 的 `commit_step` 抛 `RUN_LEASE_LOST`,数据库没有变化。
- `list_events(after_seq)` 只返回之后的事件,`seq` 递增。
- `build.py`:接上 SQLite 检查点。
- `tests/test_graph_resume.py`:用假模型(每次调用加一点延迟)运行,跑完 2 个节点后取消任务(模拟崩溃);再用同一个 `thread_id` 继续运行,断言最终完成、已完成的节点没有重复执行(统计假模型被调用的次数)。
### 第 2 天:带持久化的编排循环
**完成标准**:能讲清检查点保存了什么、恢复时从哪里继续、正在执行的节点恢复后会怎样。
### 第 2 天:业务表和后台作业
**要建的文件**
- `src/agent_mini/orchestrator.py` 新增:
- `async def drive(self, store, run_id, worker_id, should_stop) -> RunStatus`:
1. 从数据库读 `run` 和 `turns`(**每次恢复都从数据库重建状态,不依赖内存**)。
2. 循环:检查 `should_stop()` 和数据库里的状态(被取消就退出)→ `step()` → `commit_step()` → 续租。
3. `AgentError` 时把任务置为 `failed`,写 `error_code`、`error_message`,追加 `run_failed` 事件。
4. 结束、等人、失败时释放租约。
- `src/agent_mini/report.py`:`def build_report(run, turns) -> str`:Markdown,包含问题、每一步(角色、指令、回答、模型、耗时)、结束原因、最终结论、生成时间。报告完全由数据库数据拼出,不再调模型,这样结果可追溯(对应重点 E)。
- `cli.py`:`agent-mini run "问题"`(不带 `--memory` 时写数据库,打印 run_id)、`agent-mini show <run_id>`(打印报告)、`agent-mini list`。
**运行方式**:`uv run agent-mini run "..."`,然后 `uv run agent-mini show <run_id>`。
### 第 3 天:RunManager 后台作业和看门狗
**学习内容**
- 后台任务和 HTTP 请求分开:请求只负责创建任务并返回,真正的编排由后台任务执行。
- 看门狗:定期扫描“该有人跑但没人跑”的任务(服务刚启动、别的进程崩溃后租约过期)。
- 优雅关机:停止看门狗、取消后台任务、释放租约。
**要建的文件**
- `src/agent_mini/events.py`:`class EventNotifier`:每个 run 一个 `asyncio.Event`,`notify(run_id)` 唤醒等待的 SSE 连接,`wait(run_id, timeout)` 等待。只是加速,没有它 SSE 也能靠轮询数据库工作。
- `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`:
- `worker_id`:进程启动时生成(主机名 + 进程号 + 随机串)。
- `async def submit(run_id)`:`try_acquire` 成功才 `create_task(self._drive(run_id))`,任务对象存到 `self._tasks` 字典,完成后自动移除。
- `async def _drive(run_id)`:调用 `orchestrator.drive`,捕获所有异常并记录日志,保证不会让后台任务静默死掉。
- `async def watchdog()`:每 `lease_seconds / 3` 秒执行一次 `find_resumable` → `submit`。
- `async def cancel(run_id)`:数据库置为 `cancelled` + 取消本进程里对应的任务。
- `async def shutdown()`。
- `submit(run_id)`:领取成功才启动后台任务,任务对象保存起来。
- `_drive(run_id)`:用 `thread_id = run_id` 流式运行图;每收到一个节点更新就写一条 `events`;定期续租;正常结束、中断、出错时更新 `runs` 并释放租约。
- `watchdog()`:定期找“该有人跑但没人跑”的任务重新提交。
- `cancel(run_id)`、`shutdown()`。
- `tests/test_run_store.py`:两个 worker 抢同一任务只有一个成功;租约过期后另一个能抢到。
### 第 4 天:崩溃恢复测试和演示脚本
**要建的文件**
- `tests/test_runner_resume.py`:
- 用 FakeLLM(`delay_s=0.2`)和短租约(1 秒)启动 manager A 跑任务;跑完 2 步后直接取消 A 的后台任务且**不释放租约**(模拟进程被杀)。
- 启动 manager B(新的 `worker_id`),等待租约过期,看门狗把任务领走并跑完。
- 断言:最终 `completed`;`turns` 的 `step` 从 1 开始连续、没有重复;事件里出现过两个不同的 worker。
- 再测一个:A 还活着时 B 不能抢走任务。
- `scripts/demo_crash_resume.sh`:
1. 用 `FAKE_DELAY_S=2` 后台启动服务,记下进程号;
2. `curl -X POST /runs` 创建任务;
3. 等 5 秒,`kill -9` 杀掉服务;
4. 打印此时数据库里的步骤数;
5. 重新启动服务,轮询 `/runs/{id}` 直到 `completed`;
6. 打印报告,显示步骤连续不重复。
**运行方式**:`uv run pytest tests/test_runner_resume.py -v`;`bash scripts/demo_crash_resume.sh`
### 第 5 天:FastAPI 接口
**要建的文件**
- `uv add fastapi uvicorn`
- `src/agent_mini/api/app.py`:
- `def create_app(settings=None, llm=None) -> FastAPI`:可以注入 FakeLLM 方便测试。
- `lifespan`:打开存储 → 创建 `RunManager` → 启动看门狗 → `yield` → `shutdown()` → 关闭存储和 LLM 客户端。
- 全局异常处理:`AgentError` 转成统一错误响应(`RUN_NOT_FOUND` → 404,`RUN_INVALID_STATE` → 409)。
- `src/agent_mini/api/routes_runs.py`:接口表里除 SSE 外的所有接口。
- `POST /runs/{id}/input`:状态必须是 `awaiting_input`;把回答写成一条 `role=human` 的 turn,状态改回 `queued`,追加 `input_received` 事件,再 `submit`。
- `POST /runs/{id}/retry`:状态必须是 `failed`;可选更换 `model`;清掉错误字段,状态改为 `queued`,`submit`。
- `cli.py`:`agent-mini serve --port 8000`(内部调用 uvicorn)。
**运行方式**:`uv run agent-mini serve`,打开 `http://localhost:8000/docs` 用自动生成的接口文档测试。
### 第 6 天:SSE
### 第 3 天:人机协作(interrupt)和失败重试
**学习内容**
- SSE 每条消息的 `id:` 用 `events.seq`;浏览器重连时自动带 `Last-Event-ID`,服务端从这个序号之后补发。
- 心跳:长时间没有事件时发 `: ping`,避免中间的代理断开空闲连接。
- 连接断开不影响任务:SSE 只是“读”,编排在 RunManager 里。
- LangGraph 的 `interrupt`:在节点里暂停并把问题交给调用方,状态由检查点保存;之后用“恢复命令”带上人的回答继续,`interrupt` 的返回值就是人的回答。
- 注意:恢复时这个节点会**从头重新执行**,所以 `interrupt` 之前不要放有副作用的操作。
- 什么时候必须停下来问人(重点 D):信息不足、要执行有副作用或高成本的操作、多次失败。
**要建的文件**
- `src/agent_mini/api/sse.py`:`GET /runs/{id}/events`:
- 起点:`Last-Event-ID` 请求头 > `?after=` 参数 > 0。
- 循环:从数据库取 `seq > last` 的事件逐条发出 → 任务已是终态且没有新事件,发 `event: end` 后结束 → 否则 `notifier.wait(run_id, timeout=15)`,超时就发 `: ping`。
- 每轮检查 `request.is_disconnected()`,断开就退出生成器。
- 响应头:`Cache-Control: no-cache`、`X-Accel-Buffering: no`(经过 Nginx 时不缓冲)。
- `tests/test_api_sse.py`(用 `with TestClient(create_app(llm=FakeLLM())) as client:` 触发 lifespan):
- 创建任务 → 读 SSE 直到 `end`,事件顺序符合预期,最后一个业务事件是 `run_completed`。
- 带 `Last-Event-ID: 3` 再读一次,只收到 `seq > 3` 的事件。
- 不存在的 run 返回 404。
- `ask_human` 场景:状态变成 `awaiting_input` 后调 `/input`,任务能继续跑完。
- `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` 可以换模型后从检查点继续(对应工作中“额度不足 → 换模型恢复”的真实问题)。
- 测试:中断 → 回答 → 完成;模型报错 → 失败 → 换模型重试 → 完成。
### 第 7 天:极简页面和端到端走查
### 第 4 天:FastAPI 接口和 SSE
**要建的文件**
- `web/index.html`(原生 JS,不超过 200 行;你熟悉 Vue / React 的话之后可以重写,但本周先用最简单的):
- 输入问题 → `POST /runs` → 用 `EventSource` 订阅。
- 时间线:每一步显示角色、指令、回答;结束后显示结论和“查看报告”链接。
- `awaiting_input` 时出现输入框,提交到 `/input`。
- `run_failed` 时显示错误码、中文提示和“重试”按钮(可填新模型名)。
- 显示连接状态(已连接 / 重连中)。
- `api/app.py`:`GET /` 返回这个页面。
- `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。
**手动走查**:
1. 正常跑完一次;
2. 跑到一半关掉标签页,过一会儿打开同一任务,能看到完整时间线;
3. 跑到一半 `Ctrl+C` 停服务再启动,页面自动重连后继续收到事件;
4. 用错误的 Key 制造 `LLM_AUTH` 失败,修好配置后点“重试”成功。
### 第 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 验收**
- [ ] `uv run pytest` 全部通过,包括崩溃恢复和 SSE 测试
- [ ] `demo_crash_resume.sh` 演示成功
- [ ] 走查 4 个场景全部符合预期
- [ ] 能说清楚:权威数据在哪(B)、为什么编排不能放在浏览器里(C)、什么时候停下来问人(D)、结果如何变成可审计的报告(E)、怎么防止双跑(A)
- [ ] 提交并推送;在总纲进度记录里勾掉第 10 周
- [ ] 测试全部通过,包括续跑、租约、SSE、中断恢复
- [ ] 崩溃恢复演示脚本成功
- [ ] 6 个走查场景全部符合预期
- [ ] 能讲清:两层状态(检查点 vs 业务表)、为什么编排不能放在浏览器里、什么时候停下来问人、怎么防双跑、长期记忆写什么不写什么
- [ ] 在总纲进度记录里勾掉第 13 周
---
## 第 11 周 · M4:文档、架构图、映射表
## 第 14 周 · M4:评测 + 文档 + 低代码平台体验
### 第 1 天:架构图
### 第 1 天:评测集
**学习内容**
- Agent 评测比 RAG 更难,因为过程有多步。从三个层面看:
- **结果**:最终结论是否回答了问题、是否符合约束。
- **过程**:有没有该调的工具没调、不该调的乱调;轮数是否合理;该问人时有没有问。
- **成本和稳定性**:耗时、token 数、失败率。
- 检查方式:能用规则判断的用规则(调了哪些工具、轮数、是否包含关键词);不能的用“模型按评分标准打分”,并抽查一部分人工复核,看两者是否一致。
**要建的文件**
- `docs/architecture.md`,三张 mermaid 图:
1. 组件图:浏览器 → FastAPI(routes / SSE)→ RunManager → Orchestrator → 角色 → LLMClient;存储层和 EventNotifier 的位置。
2. 状态图(`stateDiagram-v2`):6 个状态和所有转换条件。
3. 时序图(`sequenceDiagram`):创建任务 → 跑两步 → 进程崩溃 → 新进程看门狗领取 → 续跑 → SSE 重连补发。
- 回头对照第 5 周画的 `notes/01-architecture.md`(公司系统的脱敏图),在 `architecture.md` 末尾写一段“和生产系统相比,我简化了什么”。
- `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 天:README 主体
### 第 2 天:用评测驱动改进
- 跑基线,挑失败最多的一类问题,改提示词或路由规则,再跑,对比结果(数字以实际跑出来的为准)。改动和结果记到 `docs/prompt-changelog.md`。
- 用假模型做一个不需要密钥的小评测子集,放进测试,防止以后改代码把流程改坏(回归测试)。
### 第 3 天:文档
**要建的文件**
- `README.md`:
- 一句话介绍 + 动图或截图(页面时间线、崩溃恢复演示)。
- 功能列表(对应上面“目标”的 5 条)。
- **10 分钟快速开始**:安装 uv → `git clone` → `uv sync` → `cp .env.example .env` → `uv run agent-mini serve` → 打开页面。默认用 FakeLLM,不需要任何密钥;再写怎么切换到真实模型。
- 配置项表。
- 接口表。
- 运行测试:`uv run pytest`。
- 崩溃恢复演示:`bash scripts/demo_crash_resume.sh`。
- 项目结构(简版)。
### 第 3 天:映射表和设计取舍
**要建的文件**
- `docs/mapping.md`,同时摘进 README(至少 5 行,生产一栏只写通用概念,不写公司接口名、表名):
- `docs/architecture.md`:组件图、业务状态机(`stateDiagram-v2`)、LangGraph 图结构(第 12 周导出的)、崩溃恢复时序图(`sequenceDiagram`)。对照第 9 周画的公司系统脱敏图,写一段“我简化了什么”。
- `docs/mapping.md`(同时摘进 README,生产一栏只写通用概念,不写公司接口名、表名):
| 生产系统里的概念(通用叫法) | 本项目里的实现 | 对应重点 |
| --- | --- | --- |
| 席位 / 专家角色 | `roles/proposer.py`、`roles/critic.py` | A |
| 主持人派活 | `Coordinator.decide()` 返回 `Decision(action="dispatch")` | A |
| 共识 / 结束条件 | `VERDICT: APPROVE` + 最大轮数,规则写在 `Orchestrator.step()` | A |
| 草稿(编排中间状态) | `runs` + `turns` 表,每步一个事务提交 | B |
| 后台作业 | `RunManager` 后台任务 + 租约 + 看门狗 | A / C |
| 流式输出 | `GET /runs/{id}/events`,按 `seq` 断点补发 | C |
| 意图澄清 / 等待用户输入 | `ask_human` → `awaiting_input` → `POST /input` | D |
| 席位 / 专家角色 | `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` 从数据库拼出的 Markdown | E |
| 过程报告 | `report.py` | E |
| 外部能力接入 | MCP 服务 `knowledge_server` | E |
- `docs/design-decisions.md`,每条写“选择 / 理由 / 代价”:
1. 编排放在服务端后台,而不是浏览器循环(关页、刷新、断网都不影响)。
2. 每步一个事务 + `(run_id, step)` 主键:写入恰好一次,模型调用至少一次。
3. 租约而不是进程内锁:多进程、多机器也不会双跑;代价是崩溃后要等租约过期才恢复。
4. 数据库是权威,SSE 只是视图:断线重连靠 `seq` 补发。
5. 结束条件写在代码里,不只靠模型判断。
6. SQLite 而不是 PostgreSQL:方便别人跑;存储层有协议隔离,换 PostgreSQL 只需新写一个实现(可选练习:用第 3 周学的 PostgreSQL 写 `pg_store.py`)。
7. 没做的事(诚实写):token 级流式输出、多实例下的事件通知(目前靠轮询兜底)、鉴权、评测。
- `docs/design-decisions.md`:每条写“选择 / 理由 / 代价”:两层状态;编排在后台;租约防双跑;节点内调用至少一次、副作用工具要幂等;结束条件写在代码里;SQLite 检查点只用于演示,生产换 PostgreSQL;长期记忆的写入规则;没做的事(鉴权、多实例下的实时通知、更完整的评测)。
### 第 4 天:从零验证
### 第 4 天:README 和从零验证
- 在一个新目录(最好是另一台电脑,或者请朋友)`git clone` 下来,严格按 README 操作并计时,目标 10 分钟内跑通。
- 遇到的每个卡点都改进 README 或代码。
- `uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run pytest` 全部通过。
- 最后检查一遍脱敏:`git grep` 搜公司名、内网域名、IP、密钥前缀,确认没有。
- `README.md`:一句话介绍 + 动图;功能列表;**10 分钟快速开始**(假模型模式,不需要密钥);切换真实模型;连接 `rag-qa` 和 MCP;接口表;评测结果表;崩溃恢复演示;映射表;项目结构。
- `Dockerfile`、`docker-compose.yml`:`agent-mini` + MCP 服务,可选连接 `rag-qa`。
- 在新目录(最好换一台电脑或请朋友)`git clone` 后按 README 操作并计时,目标 10 分钟内跑通;卡住的地方都改进。
- 脱敏检查:`git grep` 搜公司名、内网域名、IP、密钥前缀。
### 第 5 天:演示和讲解准备
### 第 5 天:讲解稿
- 录一段 2~3 分钟的演示(屏幕录制即可):提问 → 看时间线 → 杀进程 → 重启续跑 → 查看报告。
- 写 `docs/talk.md`:3 分钟讲解稿,结构是“问题(为什么多 Agent 难做可靠)→ 设计(状态机、权威源、后台作业、租约)→ 演示 → 取舍”,并引用 1~2 张案例卡说明“这些设计来自我在工作中踩过的坑”。这份稿子第 12 周会再压缩成面试口述稿。
- 录一段 2~3 分钟的演示:提问 → 工具调用 → 杀进程 → 重启续跑 → 问人 → 报告 → 评测结果。
- `docs/talk.md`:3 分钟讲解稿,“问题(为什么多 Agent 难做可靠)→ 设计(两层状态、后台作业、租约、中断、记忆)→ 演示 → 评测和取舍”,引用 1~2 张案例卡说明“这些设计来自我在工作中踩过的坑”。
### 第 6~7 天:缓冲 / 可选扩展
### 第 6~7 天:Dify / Coze 体验(低代码平台)
先确认 M1~M4 的验收全部完成。**没完成就用这两天补,不做下面的扩展。**
**为什么**:招聘里经常出现 Dify、Coze、RAGFlow、FastGPT 这类平台。不需要精通,但要能说清楚它们能做什么、和写代码比各有什么优缺点,最好亲手搭过一个。
可选 M5(二选一,做一个就够,复用第 1 个月的 Go 技能):
**做什么(二选一为主,另一个看看文档即可)**
- **Dify**(开源,可自己部署):在你的 Ubuntu 服务器上用官方的 Docker Compose 部署(注意服务器内存是否够用,以官方要求为准),或用官方云版。
- **Coze(扣子)**:直接用在线版。
- **Go 网关**:新建 `zhuangang/agent-gateway/`,Gin 服务:
- 复用 `task-board` 的 `auth` 包做登录和 JWT 校验;
- `/api/runs/*` 用 `httputil.ReverseProxy` 转发到 Python 服务,设置 `FlushInterval = -1` 让 SSE 不被缓冲;
- 转发时加上 `X-User-ID` 请求头,Python 端 `runs` 表加 `owner_id`,只能看自己的任务。
- 讲点:“Go 做接入层(鉴权、限流、转发),Python 做 Agent 编排”。
- **Vue / React 前端**:用你最熟悉的框架重写 `web/index.html`,加上任务列表、报告页、错误提示,体现前端功底。
**要搭的东西**
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 全部通过,脱敏检查完成
- [ ] 提交并推送;在总纲进度记录里勾掉第 11 周和“阶段 3”,填上“迷你项目路径:`zhuangang/agent-mini/`”
- [ ] 有评测集和至少两次对比结果
- [ ] 架构图和 README 一致;测试、ruff、pyright 通过;脱敏检查完成
- [ ] Dify 或 Coze 至少搭过一个知识库应用和一个工作流,写了对比笔记
- [ ] 在总纲进度记录里勾掉第 14 周和“阶段 3”,填上“迷你项目路径:`zhuangang/agent-mini/`”
---
## 和案例卡的关系
第 6~7 周每张案例卡都有一栏“若重做的设计”。动手前把这些结论列出来,逐条对应到本项目的某个模块或某条设计取舍;对应不上的,在 `docs/design-decisions.md` 里说明为什么这次没做。面试时就能讲成:“我在工作中遇到 X 问题,根因是 Y,所以在自己的项目里用 Z 解决,这是代码和演示。”
第 10 周每张案例卡都有一栏“若重做的设计”。动手前把这些结论列出来,逐条对应到本项目的某个模块或某条设计取舍;对应不上的,在 `docs/design-decisions.md` 里说明为什么这次没做。面试时就能讲成:“我在工作中遇到 X 问题,根因是 Y,所以在自己的项目里用 Z 解决,这是代码和演示。”
## 附:如果阶段 0 改选了 LangGraph
## 可选扩展(第 15 周有余力再做,二选一)
只在第 5 周盘点时明确改选 LangGraph 才看这一节,**不要两个都做**。概念大致对应如下(具体 API 以 LangGraph 官方文档为准):
| 自写版 | LangGraph 里的对应 |
| --- | --- |
| `Orchestrator.step()` 的规则 | 图的节点 + 条件边 |
| `Run` / `turns` 状态 | 图的 State(TypedDict 或 pydantic 模型) |
| `commit_step` 每步持久化 | Checkpointer(有 SQLite / PostgreSQL 实现) |
| 崩溃后续跑 | 用同一个 `thread_id` 从最近的检查点继续 |
| `ask_human` / `awaiting_input` | `interrupt()` 暂停,之后带上人的回答恢复 |
| SSE 事件 | 图的流式输出(`astream`)转成 SSE |
| 租约防双跑、看门狗 | 框架不直接管,仍要自己在任务表上实现 |
M1(LLM 封装和错误码)、M3 的接口和 SSE、M4 的文档和映射表都照做。
- **Go 网关**:`zhuangang/agent-gateway/`,Gin 服务,复用项目一的 JWT 鉴权;`/api/runs/*` 反向代理到 Python 服务(SSE 要关闭缓冲);加用户级限流(Redis);转发时带上用户 ID,Python 端按用户隔离任务和记忆。讲点:“Go 做接入层,Python 做 Agent 编排”。
- **Vue / React 前端**:用你熟悉的框架重写 `web/index.html`,体现前端功底。
+326
View File
@@ -0,0 +1,326 @@
# 转岗计划 · 第 7~8 周:RAG 知识库问答项目(项目二)
> 所属阶段:第二段“Python 与 AI 应用”。总纲见同目录 `README.md`,上周计划 `week06-python-basics.md`,第 9~10 周见总纲第 6 节。
## 目标
做一个“上传文档 → 基于文档问答 → 回答带出处”的知识库问答系统。招聘里 AI Agent 岗位最常见的要求之一就是 RAG 和向量数据库,这个项目专门对上它。
做完后要能演示和讲清:
1. 上传 Markdown / TXT / PDF,自动切分、向量化、入库,能删除和重建。
2. 提问后流式输出回答,每个结论标注出处 `[1]`、`[2]`,点开能看到原文片段;文档里没有的内容明确回答“文档中没有找到”。
3. 有一个小评测集,能用数字(命中率等)说明改了切分或检索方式之后效果是变好还是变差。
4. 支持多轮追问(短期记忆)。
5. Redis 做缓存和调用限流,Docker Compose 一键部署到服务器。
项目名:`rag-qa`,放在 `zhuangang/rag-qa/`。
文档素材用**公开资料或自己的学习笔记**(比如本计划的文件、Go / PostgreSQL 官方中文文档的若干页),**不要用公司文档**。
## 技术选型
| 用途 | 选择 | 说明 |
| --- | --- | --- |
| 语言 / 依赖 | Python 3.12 + uv | |
| Web 服务 | FastAPI | 招聘里 FastAPI、Flask 都常见 |
| 向量数据库 | **Chroma**(默认) | 招聘里点名的主要是 Milvus 和 Chroma。Chroma 上手最快,可以嵌入式运行,也可以跑成独立服务 |
| 其他向量库 | Milvus(了解 + 可选实现)、pgvector(可选) | 通过 `VectorStore` 协议隔离,换库只写一个新实现。第 8 周第 7 天对比说明 |
| 大模型 / 向量化 | OpenAI 兼容接口(对话 + embeddings) | 国内很多模型服务都提供兼容接口;也可以选本地向量模型。测试用假实现 |
| 关键词检索 | rank-bm25 + jieba 分词 | 第 8 周做混合检索 |
| 框架 | 第 7 周**手写**主流程;第 8 周用 **LangChain** 重写一遍做对照 | 招聘要求 LangChain 实际经验,但先手写才知道它替你做了什么 |
| 缓存 / 限流 / 会话 | Redis | 第 5 周学过 |
| 前端 | 你熟悉的 Vue 或 React | 体现前端功底 |
| 部署 | Docker Compose | |
---
## 最终项目结构
```
rag-qa/
├── README.md
├── pyproject.toml
├── .env.example
├── docker-compose.yml # api、chroma、redis、frontend
├── Dockerfile
├── data/ # 运行时数据(不提交)
├── samples/ # 演示用的公开文档
├── src/rag_qa/
│ ├── __init__.py
│ ├── config.py
│ ├── errors.py # 错误码
│ ├── llm/
│ │ ├── chat.py # 对话接口:普通 + 流式
│ │ ├── embeddings.py # 向量化接口:批量、重试
│ │ └── fake.py # 测试用假实现
│ ├── ingest/
│ │ ├── loaders.py # txt / md / pdf → 文本 + 元数据
│ │ ├── chunker.py # 按标题和长度切分,带重叠
│ │ └── pipeline.py # 加载 → 切分 → 去重 → 向量化 → 入库
│ ├── vectorstore/
│ │ ├── base.py # VectorStore 协议
│ │ ├── chroma_store.py
│ │ └── milvus_store.py # 可选
│ ├── retrieval/
│ │ ├── vector.py # 向量检索
│ │ ├── bm25.py # 关键词检索
│ │ └── hybrid.py # 融合排序(RRF)
│ ├── qa/
│ │ ├── prompts.py # 提示词
│ │ ├── answer.py # 检索 + 生成 + 引用
│ │ └── rewrite.py # 多轮追问时改写问题
│ ├── memory/
│ │ └── session.py # 会话短期记忆(Redis)
│ ├── cache.py # 向量缓存、答案缓存
│ ├── ratelimit.py
│ ├── lc_pipeline.py # LangChain 版对照实现
│ ├── api/
│ │ ├── app.py
│ │ ├── routes_documents.py
│ │ └── routes_ask.py # 含 SSE
│ └── cli.py # rag-qa ingest / ask / eval
├── eval/
│ ├── questions.jsonl # 评测集
│ ├── run_eval.py
│ └── results.md # 每次实验的结果记录
├── frontend/ # Vue / React
└── tests/
├── test_chunker.py
├── test_pipeline.py
├── test_retrieval.py
├── test_answer.py
└── test_api.py
```
---
## 第 7 周:手写 RAG 主流程
### 第 1 天:概念和项目初始化
**学习内容**
- RAG 是什么:先从资料里**检索**相关片段,再让模型**基于这些片段生成**回答。解决模型不知道私有资料、容易编造的问题。
- 两条流水线:入库(加载 → 切分 → 向量化 → 存储)和问答(问题向量化 → 检索 → 拼提示词 → 生成)。
- 向量(embedding)是什么:把一段文字变成一串数字,意思相近的文字数字也相近;相似度一般用余弦相似度。
- 选好要用的模型服务:确认它的对话接口和 embeddings 接口、向量维度、价格和限流规则。
**要建的文件**
- `uv init --package rag-qa`;`uv add fastapi uvicorn httpx pydantic pydantic-settings chromadb pypdf`;`uv add --dev pytest pytest-asyncio ruff pyright`。
- `src/rag_qa/config.py`:`LLM_BASE_URL`、`LLM_API_KEY`、`CHAT_MODEL`、`EMBED_MODEL`、`EMBED_PROVIDER`(`api` / `fake`)、`CHROMA_PATH`、`TOP_K`、`CHUNK_SIZE`、`CHUNK_OVERLAP`。
- `src/rag_qa/errors.py`:错误码沿用第 6 周的思路(超时、限流、额度、鉴权、输出异常、文档解析失败)。
- `src/rag_qa/llm/chat.py`:`async def chat(messages) -> str`、`async def chat_stream(messages) -> AsyncIterator[str]`(解析流式响应里的增量内容)。
- `src/rag_qa/llm/embeddings.py`:`async def embed(texts: list[str]) -> list[list[float]]`,按批次发送(每批条数可配置),失败按错误码重试。
- `src/rag_qa/llm/fake.py`:`FakeEmbeddings`(用文本哈希生成固定向量,同一段文字结果永远一样)、`FakeChat`。
- `.env.example`、`.gitignore`(`.env`、`data/`)。
**运行方式**:写一个小脚本,把三句话向量化后两两计算余弦相似度,看意思相近的是不是分数更高。
**完成标准**:能用自己的话讲清 RAG 的两条流水线。
### 第 2 天:加载和切分
**学习内容**
- 切分为什么重要:太大检索不准、浪费上下文;太小丢失上下文。重叠(overlap)避免一句话被切断。
- 按结构切:Markdown 先按标题切,段落太长再按长度切;每个片段带上所属标题,检索和引用都更准。
- 元数据:文档 ID、文件名、标题路径、片段序号、内容哈希。
**要建的文件**
- `src/rag_qa/ingest/loaders.py`:
- `def load_file(path: Path) -> Document`:按扩展名分派;PDF 用 pypdf 按页提取并记录页码。
- `class Document(BaseModel)`:`doc_id`、`filename`、`text`、`pages: list[PageSpan] | None`。
- `src/rag_qa/ingest/chunker.py`:
- `class Chunk(BaseModel)`:`chunk_id`、`doc_id`、`text`、`heading_path`、`index`、`page`、`content_hash`。
- `def split_markdown(doc, chunk_size, overlap) -> list[Chunk]`
- `def split_plain(doc, chunk_size, overlap) -> list[Chunk]`:优先在句号、换行处断开。
- `tests/test_chunker.py`:片段长度不超过上限、相邻片段有重叠、标题路径正确、空文档返回空列表。
**完成标准**:拿 `samples/` 里的一份 Markdown 切一下,打印片段,人工检查断句是否合理。
### 第 3 天:向量库和入库流水线
**学习内容**
- Chroma 的基本用法:`PersistentClient(path=...)`、`get_or_create_collection`、`add` / `upsert`、`query`、`delete`;按元数据过滤。
- 幂等入库:同一份文档重复上传不产生重复片段(用内容哈希做片段 ID);文档更新时先删旧片段再写新片段。
**要建的文件**
- `src/rag_qa/vectorstore/base.py`:`class VectorStore(Protocol)`:`upsert(chunks, vectors)`、`query(vector, top_k, where=None) -> list[ScoredChunk]`、`delete_by_doc(doc_id)`、`count()`。
- `src/rag_qa/vectorstore/chroma_store.py`:`class ChromaStore`,相似度用余弦(建集合时指定)。
- `src/rag_qa/ingest/pipeline.py`:`async def ingest_file(path, store, embedder) -> IngestResult`(片段数、新增数、跳过数、耗时)。
- `src/rag_qa/cli.py`:`rag-qa ingest samples/`、`rag-qa stats`。
- `tests/test_pipeline.py`:用 `FakeEmbeddings` 和临时目录;同一文件入库两次,片段数不变;删除文档后数量归零。
**运行方式**:`uv run rag-qa ingest samples/ && uv run rag-qa stats`
### 第 4 天:检索和生成
**学习内容**
- Top-K 检索 + 相似度阈值:分数太低的片段不要,宁可回答“没找到”。
- 提示词(Prompt)和上下文工程(Context Engineering):系统提示里规定“只能根据资料回答、每个结论标注编号、资料里没有就明确说没有”;资料按编号拼进上下文,注意总长度。
- 引用:模型输出的 `[1]` 对应第 1 个片段,返回时附上片段原文、文件名、标题、页码。
**要建的文件**
- `src/rag_qa/retrieval/vector.py`:`async def retrieve(question, store, embedder, top_k, min_score) -> list[ScoredChunk]`。
- `src/rag_qa/qa/prompts.py`:系统提示词、拼接上下文的函数(超过长度上限就丢掉排名靠后的片段)。
- `src/rag_qa/qa/answer.py`:
- `class Answer(BaseModel)`:`text`、`citations: list[Citation]`、`retrieved: int`、`latency_ms`。
- `async def answer(question, ...) -> Answer`;`async def answer_stream(question, ...) -> AsyncIterator[AnswerEvent]`(先发 `citations` 事件,再逐段发 `delta`,最后 `done`)。
- 一个片段都没检索到时,不调模型,直接返回“文档中没有找到相关内容”。
- `cli.py`:`rag-qa ask "问题"`。
- `tests/test_answer.py`:没检索到时不调用模型;引用编号和片段对应。
**完成标准**:问 5 个文档里有答案的问题、3 个没有答案的问题,记录表现到 `eval/results.md`(这是第 8 周评测集的雏形)。
### 第 5 天:FastAPI 接口
**要建的文件**
- `src/rag_qa/api/app.py`:`create_app()`,`lifespan` 里初始化向量库和客户端;统一错误响应。
- `src/rag_qa/api/routes_documents.py`:
- `POST /documents`:上传文件(限制类型和大小),保存后入库,返回入库结果。
- `GET /documents`:文档列表(文件名、片段数、上传时间)。
- `DELETE /documents/{doc_id}`。
- `src/rag_qa/api/routes_ask.py`:
- `POST /ask`:一次性返回 `Answer`。
- `POST /ask/stream`:SSE,事件类型 `citations` / `delta` / `done` / `error`。(`EventSource` 只支持 GET,前端用 `fetch` 读流,这一点在 README 里说明。)
- `tests/test_api.py`:上传 → 列表 → 提问 → 删除;上传不支持的文件类型返回 400。
**运行方式**:`uv run uvicorn rag_qa.api.app:create_app --factory --reload`,打开 `/docs`。
### 第 6 天:前端
**要建的文件**(`frontend/`,Vite + 你熟悉的框架)
- `src/api/rag.ts`:`uploadDocument`、`listDocuments`、`deleteDocument`、`askStream`(用 `fetch` + `ReadableStream` 解析 SSE)。
- `src/pages/DocumentsPage`:上传(支持拖拽)、列表、删除。
- `src/pages/ChatPage`:对话界面,流式显示回答;回答里的 `[1]` 可点击,侧边栏显示对应原文片段和出处。
- `src/components/CitationPanel`、`MessageItem`、`UploadBox`。
**完成标准**:上传一份文档,马上能提问,回答流式显示,引用可点开。
### 第 7 天:测试和第一版 README
- 补全测试,`uv run ruff check . && uv run pyright && uv run pytest` 通过。
- `README.md` 第一版:功能、架构图(两条流水线,mermaid)、本地运行步骤(默认可用 fake 模式跑通界面)。
- 提交并推送到 `zhuangang` 仓库(路径 `rag-qa/`);在总纲进度记录里勾掉第 7 周。
---
## 第 8 周:评测、优化、记忆、缓存、LangChain 对照、部署
### 第 1 天:评测集(先有尺子,再谈优化)
**学习内容**
- 没有评测,“优化”就只是感觉。检索和回答分开评:
- 检索:正确片段有没有出现在前 K 个里(命中率,Hit@K);正确片段排在第几(MRR)。
- 回答:是否正确、是否只用了资料里的信息、引用是否对得上。先用人工打分,再试试让模型按评分标准打分(LLM-as-judge),对比两者是否一致。
**要建的文件**
- `eval/questions.jsonl`:自己写 30 条,每条 `{"id", "question", "expected_doc", "expected_keywords", "answerable": true/false}`;其中至少 5 条是文档里**没有**答案的。
- `eval/run_eval.py`:跑全部问题,输出 Hit@3、Hit@5、MRR、无答案题的正确拒答率、平均耗时;结果追加到 `eval/results.md`,每次记录配置(切分大小、Top-K、检索方式、模型)。
- `cli.py`:`rag-qa eval`。
**完成标准**:得到一组基线数字,写在 `eval/results.md` 第一行。之后每做一次改动都重跑并记录。
### 第 2 天:检索优化
**学习内容**
- 混合检索:向量检索擅长“意思相近”,关键词检索(BM25)擅长专有名词、编号、代码。中文要先分词(jieba)。
- 融合排序:RRF(倒数排名融合),简单、不用调分数权重。
- 重排(rerank):先粗取 20 个,再用重排模型精排出前 5 个。有可用的重排接口就试,没有就只了解原理。
**要建的文件**
- `uv add rank-bm25 jieba`
- `src/rag_qa/retrieval/bm25.py`:`class Bm25Index`:从向量库里取出全部片段建索引(文档变动时重建),`search(query, top_k)`。
- `src/rag_qa/retrieval/hybrid.py`:`def rrf(results_lists, k=60) -> list[ScoredChunk]`;`async def hybrid_retrieve(...)`。
- 配置项 `RETRIEVAL_MODE=vector|bm25|hybrid`。
- `tests/test_retrieval.py`:RRF 的排序结果符合手算。
**完成标准**:在评测集上对比 3 种检索方式和 2~3 种切分大小,结果表写进 `eval/results.md`,并写一句结论(数字以你实际跑出来的为准)。
### 第 3 天:多轮追问和短期记忆
**学习内容**
- 多轮对话的问题:“那它的缺点呢?”直接拿去检索是查不到的,要先结合历史把问题改写完整(query rewrite)。
- 记忆分两种:**短期记忆**(当前会话最近几轮,存 Redis,设过期时间);**长期记忆**(跨会话记住用户偏好等,第 13 周在 Agent 项目里做)。
- 上下文长度管理:历史太长时只保留最近 N 轮,或者把更早的内容压缩成摘要。
**要建的文件**
- `uv add redis`
- `src/rag_qa/memory/session.py`:`class SessionMemory`:`append(session_id, role, content)`、`recent(session_id, n)`,Redis List + 过期时间。
- `src/rag_qa/qa/rewrite.py`:`async def rewrite_question(history, question) -> str`;没有历史时直接返回原问题,不调模型。
- `/ask/stream` 请求体增加 `session_id`;前端每个对话窗口生成一个 `session_id`。
**完成标准**:连续追问 3 轮,第 2、3 轮仍能检索到正确片段;日志里能看到改写后的问题。
### 第 4 天:Redis 缓存和限流
**要建的文件**
- `src/rag_qa/cache.py`:
- 向量缓存:key = `emb:{模型名}:{文本哈希}`,同一段文字不重复向量化(重新入库时省钱)。
- 答案缓存:key = `ans:{问题哈希}:{知识库版本号}`;任何文档增删时知识库版本号加 1,旧缓存自然失效。多轮对话(有历史)不走答案缓存。
- `src/rag_qa/ratelimit.py`:按 IP 或会话限制提问频率(固定窗口,和第 5 周 Go 版同一个思路)。
- Redis 不可用时降级:跳过缓存和记忆,问答照常工作。
**完成标准**:同一问题第二次明显更快(日志里有命中标记);重复入库同一文档时 embeddings 接口不再被调用。
### 第 5 天:LangChain 对照实现
**学习内容**
- 招聘里普遍要求 LangChain / LangGraph 的实际项目经验。用 LangChain 的组件把问答链重写一遍:文本切分器、Chroma 向量库封装、检索器、对话模型、提示词模板。
- 对照着看:每个组件替你做了什么,哪些地方你手写的版本更可控。
- LlamaIndex 是另一个常见的 RAG 框架,思路类似,本周只读一下官方入门示例,能说出和 LangChain 的定位差别即可。
**要建的文件**
- `uv add langchain langchain-openai langchain-chroma langchain-text-splitters`(包名以官方文档为准)。
- `src/rag_qa/lc_pipeline.py`:用 LangChain 组件实现 `ingest` 和 `ask`,同样输出带引用的回答。
- `eval/run_eval.py` 增加 `--impl handwritten|langchain`,两种实现在同一评测集上对比。
- `docs/handwritten-vs-langchain.md`:组件对照表 + 你的结论。
**完成标准**:两种实现都能跑评测;能讲清“我为什么先手写,再用框架”。
### 第 6 天:Docker Compose 部署
**要建的文件**
- `Dockerfile`:基于 Python 官方 slim 镜像,用 uv 安装依赖,非 root 用户运行。
- `docker-compose.yml`:
- `chroma`:Chroma 官方镜像跑成独立服务,数据卷持久化;`api` 通过 HTTP 客户端连接(`ChromaStore` 支持嵌入式和服务端两种模式,由配置决定)。
- `redis`、`api`、`frontend`(nginx 托管静态文件,`/api` 反向代理到 `api`,注意关闭代理缓冲,否则流式输出会被攒成一整块)。
- 部署到 Ubuntu 服务器(同第 5 周的 `deploy.md` 流程),在 README 里补部署小节。
**完成标准**:服务器上能上传文档、流式问答。
### 第 7 天:向量库对比和收尾
**学习内容**
- 把三种向量库的定位讲清楚(只写你查证过的内容):
- Chroma:上手快,可嵌入运行,适合原型和中小规模。
- Milvus:专门的向量数据库,面向更大规模和分布式部署,招聘里和 Chroma 一起被点名;有轻量的单机方式可以本地试。
- pgvector:PostgreSQL 扩展,已经在用 PostgreSQL 的团队不用多维护一个数据库,向量和业务数据可以一起查询、一起事务。
- 有余力(可选,二选一):实现 `milvus_store.py` 或基于 pgvector 的实现,跑同一套评测。
**要建的文件和修改**
- `docs/vector-db-comparison.md`:上面的对比 + 你实际试过的部分。
- `README.md` 完整版:
- 一句话介绍、截图或动图。
- 架构图(入库流水线、问答流水线、缓存和记忆的位置)。
- **评测结果表**(来自 `eval/results.md`,每个数字都要能复现)。
- 关键设计:切分策略、混合检索、拒答策略、引用、缓存失效、降级。
- 本地运行(fake 模式无需密钥)、真实模型配置、Docker 部署。
- 已知不足(比如:没做权限隔离、表格和图片内容没解析)。
- 提交并推送;在总纲进度记录里勾掉第 8 周和“项目二”。
---
## 本项目自查清单
- [ ] 能讲清 RAG 两条流水线,以及切分大小、重叠、Top-K、阈值各自的影响
- [ ] 会用 Chroma,知道 Milvus、pgvector 的定位差别
- [ ] 回答带引用;文档里没有的能正确拒答
- [ ] 有 30 条的评测集,能用 Hit@K、MRR、拒答率说明优化效果
- [ ] 做过混合检索对比
- [ ] 多轮追问能改写问题,短期记忆存在 Redis
- [ ] 向量缓存、答案缓存、限流、降级都做了
- [ ] 用 LangChain 重写过一遍并做了对比
- [ ] Docker Compose 部署到服务器,流式输出正常
- [ ] README 完整,评测数字可复现
下一步:第 9 周开始阶段 0 盘点和阶段 1 读通画图,见总纲 `README.md` 第 6 节。这个 RAG 服务在第 11 周会被包装成 MCP 工具,给多 Agent 项目调用。
+1 -1
View File
@@ -1,6 +1,6 @@
# 转岗计划 · 第 1 周学习计划(Go 基础语法)
> 所属阶段:第 1 个月 Go 全栈基础。总纲见同目录 `README.md`。
> 所属阶段:第一段 Go 全栈基础(第 1~5 周)。总纲见同目录 `README.md`。
目标:一周内掌握 Go 的核心语法,最后用纯标准库写出一个能跑的命令行“任务清单”小程序,为第 2 周用 Gin 写接口打基础。
+1 -1
View File
@@ -1,6 +1,6 @@
# 转岗计划 · 第 2 周学习计划(Gin 写 HTTP 接口 + 接前端)
> 所属阶段:第 1 个月 Go 全栈基础。总纲见同目录 `README.md`,下周计划见 `week03-postgresql.md`。
> 所属阶段:第一段 Go 全栈基础(第 1~5 周)。总纲见同目录 `README.md`,下周计划见 `week03-postgresql.md`。
目标:把第 1 周的命令行任务清单改造成一个 Web 应用。后端用 Gin 提供 REST 接口,前端用你熟悉的 React 或 Vue 调用接口。本周数据先存在内存里,第 3 周再换成 PostgreSQL。
+28 -2
View File
@@ -1,12 +1,14 @@
# 转岗计划 · 第 3 周学习计划(PostgreSQL + SQL + sqlc)
> 所属阶段:第 1 个月 Go 全栈基础。总纲见同目录 `README.md`,上周计划 `week02-gin-api.md`,下周计划 `week04-auth-project1.md`。
> 所属阶段:第一段 Go 全栈基础(第 1~5 周)。总纲见同目录 `README.md`,上周计划 `week02-gin-api.md`,下周计划 `week04-auth-project1.md`。
目标:把第 2 周任务清单里的 `MemoryStore` 换成 PostgreSQL。学会表结构设计、常用 SQL、索引、事务和数据库迁移,用 sqlc 生成类型安全的数据访问代码。因为上周用接口隔离了存储层,本周主要是新写一个 `PgStore`,接口代码基本不用动。
前置条件:第 2 周的 `todo-api` 已完成,`go test ./... -race` 能通过,前端能联调。
要求:SQL 自己写,AI 只帮你检查和解释。本周是你在 Agent 项目里设计状态表的基础,表结构设计这一块一定要自己想清楚。
要求:SQL 自己写,AI 只帮你检查和解释。表结构设计、索引、事务是所有后端岗位的基本功,后面 RAG 和 Agent 项目设计状态表也靠它。
**为什么学 PostgreSQL 而招聘里 MySQL 更多**:招聘里写 MySQL 的明显更多,但两者的常用 SQL 大部分相同。本周用 PostgreSQL 学(它的 pgvector 扩展在 RAG 里也能用),文末附“MySQL 差异速查”和一个半天的小练习,面试时能说清差异就够了。
参考资料:
- PostgreSQL 官方文档(中文):http://www.postgres.cn/docs/current/
@@ -289,5 +291,29 @@ docker compose exec postgres psql -U todo -d todo # 进入交互模式
- [ ] 会用 `ON CONFLICT` 做“存在就跳过或更新”
- [ ] 写了能自动跳过的集成测试
- [ ] 会做游标分页
- [ ] 看过文末 MySQL 差异速查,能说出 3 个以上的差异
## 附:MySQL 差异速查(第 7 天有余力时做,或放到第 5 周的缓冲时间)
招聘里 MySQL 出现得多,会 PostgreSQL 后只需要知道这些差异:
| 方面 | PostgreSQL | MySQL 8 |
| --- | --- | --- |
| 自增主键 | `BIGSERIAL` 或 `GENERATED ... AS IDENTITY` | `BIGINT AUTO_INCREMENT` |
| 插入后拿回数据 | `INSERT ... RETURNING *` | 不支持 `RETURNING`,用 `LAST_INSERT_ID()` 再查一次 |
| 存在就更新 | `ON CONFLICT (...) DO UPDATE` | `ON DUPLICATE KEY UPDATE` |
| 布尔 | `BOOLEAN` | `TINYINT(1)`(`BOOLEAN` 是它的别名) |
| 时间 | `TIMESTAMPTZ` 带时区 | `DATETIME` 不带时区,`TIMESTAMP` 有范围限制;一般统一存 UTC |
| JSON | `JSONB`,可建索引 | `JSON`,索引要借助生成列 |
| 字符集 | 默认 UTF-8 | 建库时指定 `utf8mb4`,否则存不了表情符号 |
| 字符串比较 | 默认区分大小写 | 默认排序规则一般不区分大小写 |
| 存储引擎 | 只有一种 | 用 InnoDB(支持事务和外键) |
| 占位符(Go 驱动) | `$1, $2` | `?` |
**小练习(半天)**
- 在 `docker-compose.yml` 里加一个 `mysql:8` 服务(端口 `3306`)。
- `sql/practice/mysql_schema.sql`:把 `000001` 和 `000003` 迁移改写成 MySQL 语法,跑通。
- `sql/practice/mysql_notes.md`:记下改写时遇到的每一处差异。
- 了解即可:sqlc 也支持 MySQL(`engine: mysql`),GORM 两种数据库都支持,换库主要改的是 SQL 方言和驱动。
下周预告:第 4 周在这套基础上做项目一“团队任务看板”:注册登录、JWT、看板成员权限、按状态分列的任务,前后端完整联调。
+6 -6
View File
@@ -1,8 +1,8 @@
# 转岗计划 · 第 4 周学习计划(注册登录 + JWT + 权限 → 项目一:团队任务看板)
# 转岗计划 · 第 4 周学习计划(项目一 · 上:注册登录 + JWT + 权限 + 前端)
> 所属阶段:第 1 个月 Go 全栈基础的收官周。总纲见同目录 `README.md`,上周计划 `week03-postgresql.md`。
> 所属阶段:第一段 Go 全栈基础。项目一分两周做:本周完成功能主体,第 5 周加 Redis、消息队列和 Docker 部署(`week05-project1-redis-docker.md`)。总纲见同目录 `README.md`,上周计划 `week03-postgresql.md`。
目标:做出第一个能写进简历的全栈项目“团队任务看板”:用户注册登录,创建看板、邀请成员,看板里的任务按“待办 / 进行中 / 已完成”分三列,只有看板成员能看和改,只有看板所有者能管理成员。
目标:做出第一个能写进简历的全栈项目“团队任务看板”的功能主体:用户注册登录,创建看板、邀请成员,看板里的任务按“待办 / 进行中 / 已完成”分三列,只有看板成员能看和改,只有看板所有者能管理成员。
前置条件:第 3 周的 `todo-api` 已经能用 PostgreSQL 存储,迁移、sqlc、事务都跑通。
@@ -321,7 +321,7 @@ tasks
- 架构说明:handler / service / store 分层,权限在哪一层判断。
- 安全说明:密码哈希、JWT 过期、为什么非成员返回 404、令牌存储的取舍。
- 本地运行步骤:`docker compose up -d` → `make migrate-up` → `make seed` → `go run .` → `npm run dev`。
- 已知不足和下一步(刷新令牌、实时同步、部署),诚实写出来。
- 已知不足和下一步(刷新令牌、实时同步),诚实写出来。缓存、通知和部署下周补上。
- 提交并推送到 `zhuangang` 仓库(路径 `task-board/`),确认 `.env` 和密钥没有提交。
**完成标准**
@@ -329,7 +329,7 @@ tasks
- `go test ./... -race` 和 `make test-integration` 都通过。
- 在一个新目录里 `git clone` 下来,按 README 能从零跑通。
- 能用 3 分钟讲清楚这个项目:做了什么、分层、权限怎么保证、遇到过什么问题。
- 在总纲 `README.md` 的进度记录里勾掉第 4 周和“第 1 个月 Go 全栈基础”。
- 在总纲 `README.md` 的进度记录里勾掉第 4 周。
---
@@ -344,4 +344,4 @@ tasks
- [ ] 项目一 README 完整,别人能从零跑起来
- [ ] 能用 3 分钟讲清楚项目一
下一步:第 5 周进入第 2 个月,先做阶段 0 盘点和阶段 1 读通画图,具体见总纲 `README.md` 第 4 节。第 1 个月学到的分层、事务、`ON CONFLICT` 幂等写入、鉴权,在第 9~11 周的 Agent 项目里都会再用到。
下周预告:第 5 周给项目一加 Redis 缓存和限流、用 Redis Streams 做一个简单的消息队列(任务指派通知),再用 Docker Compose 打包部署到你的 Ubuntu 服务器,见 `week05-project1-redis-docker.md`。
+225
View File
@@ -0,0 +1,225 @@
# 转岗计划 · 第 5 周学习计划(项目一 · 下:Redis 缓存与限流 + 消息队列入门 + Docker 部署)
> 所属阶段:第一段 Go 全栈基础的最后一周。总纲见同目录 `README.md`,上周计划 `week04-auth-project1.md`,下周计划 `week06-python-basics.md`。
目标:给“团队任务看板”补上招聘里常见的三样东西:**Redis**(缓存、限流)、**消息队列**(用 Redis Streams 入门,了解 RabbitMQ 的基本概念)、**Docker 部署**(Docker Compose 一键启动,部署到自己的 Ubuntu 服务器,加一条 CI 流水线)。
原则:这三样都**浅学够用**。Go 全栈部分到本周结束,之后的主线是 Python 和 AI 应用。本周不追求高并发、集群这些深入话题。
前置条件:第 4 周的项目一功能完成,测试通过。
参考资料:
- Redis 命令参考(中文):https://redis.com.cn/commands.html
- go-redis:https://redis.uptrace.dev/zh/
- Redis Streams 介绍:https://redis.io/docs/latest/develop/data-types/streams/
- Docker 文档:https://docs.docker.com/get-started/
- RabbitMQ 入门教程:https://www.rabbitmq.com/tutorials
---
## 最终项目结构(只列新增和修改)
```
task-board/
├── docker-compose.yml # 全套:postgres、redis、backend、frontend(nginx)
├── docker-compose.dev.yml # 开发用:只起 postgres 和 redis
├── .env.example # 部署用的环境变量示例
├── deploy/
│ ├── nginx.conf # 前端静态文件 + /api 反向代理到后端
│ └── deploy.md # 部署到 Ubuntu 服务器的步骤
├── .ci/ # 按你的代码托管平台放到对应目录(见第 7 天)
│ └── ci.yml
├── backend/
│ ├── Dockerfile # 多阶段构建
│ ├── .dockerignore
│ ├── cmd/
│ │ ├── server/main.go # 原来的 main.go 移到这里
│ │ └── worker/main.go # 消息消费者(通知处理)
│ └── internal/
│ ├── cache/
│ │ ├── redis.go # 连接和通用 Get/Set
│ │ ├── board_cache.go # 看板任务列表缓存
│ │ └── board_cache_test.go
│ ├── ratelimit/
│ │ ├── limiter.go # 固定窗口限流
│ │ └── limiter_test.go
│ ├── middleware/
│ │ └── ratelimit.go # 登录接口限流中间件
│ ├── queue/
│ │ ├── stream.go # Redis Streams 生产和消费
│ │ └── stream_test.go
│ └── notify/
│ ├── events.go # 事件结构:任务被指派
│ ├── store.go # 通知表读写
│ └── handler.go # 消费者处理逻辑(幂等)
│ └── db/migrations/
│ ├── 000002_notifications.up.sql
│ └── 000002_notifications.down.sql
└── frontend/
├── Dockerfile # 构建静态文件,用 nginx 提供
└── src/components/NotificationBell.tsx
```
---
## 第 1 天:Redis 基础
**学习内容**
- Redis 是什么:内存里的键值数据库,常用来做缓存、计数、限流、会话、排行榜、简单队列。
- 五种常用类型:String、Hash、List、Set、Sorted Set;过期时间 `EXPIRE` / `TTL`。
- 用 `redis-cli` 动手,Go 里用 go-redis:`go get github.com/redis/go-redis/v9`。
**要建的文件**
- `docker-compose.dev.yml`:`postgres:16` + `redis:7`,Redis 端口 `6379`。原来 `backend/docker-compose.yml` 的数据库服务挪到这里。
- `backend/internal/cache/redis.go`:
- `func NewRedis(ctx context.Context, url string) (*redis.Client, error)`:连接并 `Ping`。
- `func GetJSON[T any](ctx, rdb, key string) (T, bool, error)`、`func SetJSON(ctx, rdb, key string, v any, ttl time.Duration) error`(顺便练一下 Go 泛型)。
- `internal/config/config.go`:新增 `RedisURL`。
- `backend/sql/practice/redis_cli.md`:记录你在 `redis-cli` 里敲过的命令和结果(每种类型至少 2 条)。
**运行方式**:`docker compose -f docker-compose.dev.yml up -d`,`docker compose -f docker-compose.dev.yml exec redis redis-cli`
**完成标准**:能说出 Redis 为什么快、数据放内存的风险(重启丢数据,了解 RDB 和 AOF 两种持久化即可)。
---
## 第 2 天:缓存
**学习内容**
- 旁路缓存(Cache Aside):读时先查缓存,没有再查数据库并写回缓存;写时**先更新数据库,再删除缓存**。
- 三个常见问题,面试常问:缓存穿透(查不存在的数据)、缓存击穿(热点 key 过期瞬间大量请求)、缓存雪崩(大量 key 同时过期)。各自的常用办法:缓存空值、互斥锁 / 单飞(singleflight)、过期时间加随机值。
**要建的文件**
- `internal/cache/board_cache.go`:
- `type BoardCache struct { rdb *redis.Client; ttl time.Duration }`
- `func (c *BoardCache) GetTasks(ctx, boardID int64) (map[string][]model.Task, bool, error)`
- `func (c *BoardCache) SetTasks(ctx, boardID int64, tasks map[string][]model.Task) error`:过期时间 = 基础时间 + 随机几十秒。
- `func (c *BoardCache) Invalidate(ctx, boardID int64) error`
- key 统一格式:`board:{id}:tasks`。
- 修改 `internal/service/task_service.go`:`List` 先查缓存;`Create`、`Update`、`Move`、`Delete` 成功后删除缓存。用 `golang.org/x/sync/singleflight` 合并同一看板的并发查询。
- **Redis 不可用时降级**:缓存出错只记日志,继续查数据库,不让整个接口失败。
- `internal/cache/board_cache_test.go`:没设 `TEST_REDIS_URL` 时跳过;测试读写、过期、删除。
**完成标准**:能画出旁路缓存的读写流程,能解释为什么是“删缓存”而不是“更新缓存”;停掉 Redis 后接口照常可用。
---
## 第 3 天:限流
**学习内容**
- 为什么要限流:防暴力破解登录、防刷接口、保护下游(以后调用大模型接口时同样要限流,因为有额度和费用)。
- 固定窗口计数:`INCR key` + 首次设置 `EXPIRE`;用 Lua 脚本或 `MULTI` 保证两步一起执行。了解滑动窗口和令牌桶的思路即可。
**要建的文件**
- `internal/ratelimit/limiter.go`:
- `type Limiter struct { rdb *redis.Client }`
- `func (l *Limiter) Allow(ctx, key string, limit int, window time.Duration) (allowed bool, remaining int, err error)`
- `internal/ratelimit/limiter_test.go`:同一个 key 第 `limit+1` 次被拒绝;窗口过去后恢复。
- `internal/middleware/ratelimit.go`:`func RateLimit(l *ratelimit.Limiter, keyFn func(*gin.Context) string, limit int, window time.Duration) gin.HandlerFunc`,超限返回 429,响应头带 `Retry-After`。
- `router.go`:登录接口按“IP + 邮箱”限流(比如每分钟 5 次),其他接口按用户 ID 宽松限流。
**完成标准**:用 `api.http` 连续错误登录 6 次,第 6 次返回 429;能说出固定窗口在窗口边界的问题。
---
## 第 4 天:消息队列入门(Redis Streams)
**学习内容**
- 消息队列解决什么问题:**解耦**(指派任务的接口不用关心通知怎么发)、**异步**(接口快速返回)、**削峰**。
- 基本概念:生产者、消费者、消费者组、确认(ACK)、重试、死信、“至少一次”投递导致的**重复消费**——所以消费者必须**幂等**。
- Redis Streams 命令:`XADD`、`XREADGROUP`、`XACK`、`XPENDING`、`XAUTOCLAIM`。
- RabbitMQ 只了解概念(交换机、队列、路由键、确认),对比一下:Redis Streams 轻量,适合小项目;RabbitMQ / Kafka 是专门的消息中间件,可靠性和功能更强。本周不装 RabbitMQ,读完官方入门教程的前两篇即可。
**要建的文件**
- `db/migrations/000002_notifications.up.sql` / `.down.sql`:`notifications(id, user_id, event_id TEXT UNIQUE, type, payload JSONB, read_at, created_at)`。`event_id` 唯一约束用来做幂等。
- `internal/notify/events.go`:`type TaskAssigned struct { EventID string; BoardID, TaskID, AssigneeID, ByUserID int64; At time.Time }`。
- `internal/queue/stream.go`:
- `func Publish(ctx, rdb, stream string, v any) (string, error)`:`XADD`,消息体 JSON。
- `func Consume(ctx, rdb, stream, group, consumer string, handle func(ctx, id string, body []byte) error) error`:创建消费者组(已存在就忽略)→ 循环 `XREADGROUP` → 处理成功才 `XACK`;启动时先用 `XAUTOCLAIM` 认领别的消费者挂掉后遗留的消息。
- `internal/notify/store.go`:`InsertNotification`,用 `ON CONFLICT (event_id) DO NOTHING`。
- `internal/notify/handler.go`:处理 `TaskAssigned` → 写通知表。
- 修改 `task_service.go`:创建或修改任务时负责人有变化,就 `Publish` 一个 `TaskAssigned` 事件(发布失败只记日志,不影响主流程,并在 README 里写明这个取舍)。
- `cmd/worker/main.go`:独立的消费者进程,支持优雅关机。
- 新接口:`GET /api/notifications`、`POST /api/notifications/:id/read`。
- `internal/queue/stream_test.go`:同一条消息处理两次,通知表只有一行。
**运行方式**:一个终端 `go run ./cmd/server`,另一个 `go run ./cmd/worker`;指派任务后查通知接口。
**完成标准**:能说清“至少一次投递 + 幂等消费”;把 worker 停掉再指派任务,重新启动 worker 后通知能补上。
---
## 第 5 天:Docker 镜像
**学习内容**
- 镜像、容器、卷、网络;`Dockerfile` 常用指令。
- 多阶段构建:第一阶段编译,第二阶段只放编译好的二进制,镜像小、没有源码。
- 容器里的配置都走环境变量;服务之间用 Compose 的服务名互相访问(比如 `postgres:5432`)。
**要建的文件**
- `backend/Dockerfile`:
- 阶段一 `golang:1.x` 镜像:先复制 `go.mod`、`go.sum` 下载依赖(利用缓存),再复制源码,`CGO_ENABLED=0 go build` 出 `server` 和 `worker` 两个二进制。
- 阶段二用体积小的基础镜像(`alpine` 或 `distroless`),非 root 用户运行。
- `backend/.dockerignore`
- `frontend/Dockerfile`:阶段一 `node` 镜像 `npm ci && npm run build`;阶段二 `nginx` 镜像放静态文件和 `deploy/nginx.conf`。
- `deploy/nginx.conf`:`/` 指向静态文件(单页应用的路由回退到 `index.html`),`/api/` 反向代理到 `backend:8080`。
- 前端接口地址改成相对路径 `/api`,开发时用 Vite 代理。
**运行方式**:`docker build -t task-board-backend ./backend`,`docker images` 看镜像大小。
**完成标准**:能说出多阶段构建的好处;后端镜像不包含源码和 Go 工具链。
---
## 第 6 天:Docker Compose 一键启动 + 前端通知
**要建的文件**
- `task-board/docker-compose.yml`:
- `postgres`、`redis`(都带具名卷和健康检查)。
- `migrate`:用 golang-migrate 的官方镜像执行迁移,执行完就退出。
- `backend`:`depends_on` 等数据库健康和迁移完成;环境变量从 `.env` 读。
- `worker`:同一个镜像,启动命令换成 `worker`。
- `frontend`:对外暴露 `80` 端口。
- `task-board/.env.example`:`POSTGRES_PASSWORD`、`JWT_SECRET`、`REDIS_URL` 等,写清楚哪些必须改。
- `frontend/src/components/NotificationBell.tsx`:每 30 秒拉一次通知列表,显示未读数,点开可标记已读。
**运行方式**:`cp .env.example .env`(改密码和密钥)→ `docker compose up -d --build` → 打开 `http://localhost`。
**完成标准**:一台只装了 Docker 的机器,一条命令就能跑起完整系统。
---
## 第 7 天:部署到服务器 + CI + 收尾
**学习内容**
- 部署的最小流程:服务器装 Docker → 拉代码 → 配 `.env` → `docker compose up -d --build`;更新时 `git pull` 再执行一次。
- CI:每次推送自动跑检查和测试。不同代码托管平台的配置文件位置不同(GitHub Actions 是 `.github/workflows/`,GitLab 是 `.gitlab-ci.yml`,Gitea 的 Actions 和 GitHub 写法接近),先确认你的 `git.gozeros.com` 是哪种平台、有没有开启 CI,没有的话就先写好文件、在本地用脚本跑同样的步骤。
- 了解即可:Kubernetes 是更大规模的容器编排,招聘里没有点名要求,本计划不学。
**要建的文件和修改**
- `deploy/deploy.md`:在 Ubuntu 服务器上部署的完整步骤,包括防火墙只开 80(和 22)、`.env` 权限设为 600、查看日志 `docker compose logs -f backend`、备份数据库 `pg_dump`。
- `.ci/ci.yml`(放到你平台对应的目录):后端 `go vet`、`go test -race`(起 postgres 和 redis 服务容器跑集成测试);前端 `npm ci && npm run build`;最后 `docker build` 检查镜像能构建。
- 在你的 Ubuntu 服务器上按 `deploy.md` 部署一次(服务器是内网或没有域名也没关系,能在内网访问即可;HTTPS 放到第 15 周和 AI 项目一起处理)。
- 更新 `task-board/README.md`:架构图加上 Redis、worker、Nginx;新增“缓存策略”“限流”“消息队列与幂等”“部署”四节;本地开发和一键部署两种运行方式。
- 提交并推送到 `zhuangang` 仓库。确认 `.env` 没有提交。
**完成标准**
- `go vet ./...`、`go test ./... -race` 和集成测试都通过。
- 服务器上能访问项目一,重启服务器后 `docker compose` 能自动拉起(服务加 `restart: unless-stopped`)。
- 能用 3 分钟讲清楚项目一的完整架构:分层、鉴权、缓存、限流、消息队列、部署。
- 在总纲 `README.md` 的进度记录里勾掉第 5 周和“第一段 Go 全栈基础”。
---
## 本周自查清单
- [ ] 会用 Redis 五种常用类型,知道持久化的两种方式
- [ ] 会写旁路缓存,能讲清穿透、击穿、雪崩和对应办法
- [ ] Redis 挂了接口不跟着挂(降级)
- [ ] 会用 Redis 写固定窗口限流
- [ ] 理解消息队列的作用,能讲清“至少一次投递 + 幂等消费”
- [ ] 会写 Redis Streams 的生产者和消费者组,知道 RabbitMQ 的基本概念
- [ ] 会写多阶段构建的 Dockerfile 和多服务的 Docker Compose
- [ ] 项目一已部署到服务器,有 CI 配置
下周预告:第 6 周开始 Python。从这里起 Python 是主语言,Go 是加分项。
@@ -1,12 +1,12 @@
# 转岗计划 · 第 8 周学习计划(Python 速成:给 JS 开发者)
# 转岗计划 · 第 6 周学习计划(Python 速成:给 JS 开发者)
> 所属阶段:第 2 个月最后一周,为第 9~11 周的 Python 迷你多 Agent 项目做准备。总纲见同目录 `README.md`,项目计划见 `agent-mini-project.md`。
> 所属阶段:第二段“Python 与 AI 应用”的第一周。Python 是求职的**主语言**,后面的 RAG 项目(第 7~8 周)和多 Agent 项目(第 11~14 周)都用它。总纲见同目录 `README.md`,下周计划见 `rag-project.md`。
目标:一周内把写 Agent 服务需要的 Python 补齐:基础语法、类型标注、asyncio、pydantic、httpx、pytest、FastAPI 和 SSE。不追求全面,只学项目里马上会用到的。
目标:一周内把写 AI 应用服务需要的 Python 补齐:基础语法、类型标注、asyncio、pydantic、httpx、pytest、FastAPI 和 SSE。不追求全面,只学项目里马上会用到的。
学法:你有 7 年 JS 和一个月 Go 的底子,每个知识点都先想“JS / Go 里对应的是什么”,再看 Python 的写法。每天的练习文件第一段写一张小对照表。
本周只学 Python,不开 Agent 项目(戒律:同一周不同时开新框架和新功能)。
本周只学 Python,不开 RAG 和 Agent 项目(戒律:同一周不同时开新框架和新功能)。招聘里 FastAPI 和 Flask 都常见,本计划用 FastAPI;Flask 概念更少,会了 FastAPI 再看 Flask 半天就能上手。
参考资料:
- Python 官方教程(中文):https://docs.python.org/zh-cn/3/tutorial/
@@ -151,7 +151,7 @@ py-learning/
**要建的文件**
- `uv add pydantic pydantic-settings`
- `day5_models.py`:
- `class Decision(BaseModel)`:`action: Literal["dispatch", "finish", "ask_human"]`、`target: str | None = None`、`instruction: str = ""`、`reason: str`。(这就是第 9 周协调者输出的雏形)
- `class Decision(BaseModel)`:`action: Literal["dispatch", "finish", "ask_human"]`、`target: str | None = None`、`instruction: str = ""`、`reason: str`。(这就是第 11~12 周多 Agent 项目里协调者输出的雏形)
- `def parse_decision(raw: str) -> Decision`:模型常常会在 JSON 外面包一层 ```` ```json ```` 代码块或多说几句话,先提取出第一个 `{...}` 再 `model_validate_json`;失败时抛出自定义的 `BadModelOutput`,错误信息里带上原始文本的前 200 个字。
- `class Settings(BaseSettings)`:`llm_base_url: str`、`llm_api_key: str = ""`、`llm_model: str`、`llm_timeout_s: float = 30`,从 `.env` 读取。
- `tests/test_day5.py`:参数化测试 `parse_decision`:正常 JSON、带代码块的 JSON、前后有废话的 JSON、`action` 拼错、缺少 `reason`、完全不是 JSON。
@@ -193,7 +193,7 @@ py-learning/
**学习内容**
- FastAPI 路由、路径参数、查询参数,请求体直接用 pydantic 模型(对照第 2 周的 Gin)。
- 依赖注入 `Depends`。
- `lifespan`:服务启动和关闭时执行的代码(第 10 周用它在启动时恢复未完成的任务)。
- `lifespan`:服务启动和关闭时执行的代码(第 13 周用它在启动时恢复未完成的任务)。
- 后台任务:用 `asyncio.create_task` 启动,**要把任务对象保存起来**,否则可能被垃圾回收。
- SSE 格式:`id: 1\nevent: progress\ndata: {...}\n\n`;用 `StreamingResponse(生成器, media_type="text/event-stream")` 返回;浏览器用 `EventSource` 订阅,断线会自动重连并带上 `Last-Event-ID` 请求头。
- 检测客户端断开:`await request.is_disconnected()`。
@@ -213,9 +213,9 @@ py-learning/
**完成标准**
- 启动任务后关掉页面,再打开、重新订阅,能看到任务已经继续往前跑,而且能补齐中间漏掉的事件。
- 能说清楚“编排为什么不能只活在浏览器里”——这就是重点 C,也是第 10 周要做的核心功能的缩小版。
- 能说清楚“编排为什么不能只活在浏览器里”——这就是重点 C,也是第 13 周要做的核心功能的缩小版。
- `uv run ruff check . && uv run pytest` 通过,提交并推送到 `zhuangang` 仓库(路径 `py-learning/`)。
- 在总纲 `README.md` 的进度记录里勾掉第 8 周。
- 在总纲 `README.md` 的进度记录里勾掉第 6 周。
---
@@ -230,4 +230,4 @@ py-learning/
- [ ] 会用 httpx 发异步请求、按错误类型决定是否重试,会用 `MockTransport` 测试
- [ ] 会写 FastAPI 接口和 SSE,理解后台任务和 SSE 连接是两回事
下周预告:第 9 周开始迷你多 Agent 项目,先做 M1(LLM 调用封装)和 M2(多轮编排循环),见 `agent-mini-project.md`。
下周预告:第 7~8 周做 RAG 知识库问答项目(FastAPI + Chroma),见 `rag-project.md`。本周写的 httpx 重试、pydantic 解析、SSE 都会直接用上。