From 1e6315bb4041f85c4eff04acad461c7dfcb993c8 Mon Sep 17 00:00:00 2001 From: Shikui Wang Date: Thu, 22 Oct 2026 18:17:06 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A1=E5=88=92=E8=B0=83=E6=95=B4=E4=B8=BA?= =?UTF-8?q?=2016=20=E5=91=A8=EF=BC=9A=E4=BB=A5=20Python/Agent=20=E4=B8=BA?= =?UTF-8?q?=E4=B8=BB=E7=BA=BF=EF=BC=8C=E6=96=B0=E5=A2=9E=20RAG=20=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E3=80=81Redis/=E6=B6=88=E6=81=AF=E9=98=9F=E5=88=97/Do?= =?UTF-8?q?cker=E3=80=81LangGraph=E3=80=81MCP=E3=80=81=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E3=80=81=E8=AF=84=E6=B5=8B=E5=92=8C=E5=B2=97=E4=BD=8D=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E5=AF=B9=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 4 +- plan/README.md | 442 +++++----- plan/agent-mini-project.md | 772 ++++++++---------- plan/rag-project.md | 326 ++++++++ plan/week01-go-basics.md | 2 +- plan/week02-gin-api.md | 2 +- plan/week03-postgresql.md | 30 +- plan/week04-auth-project1.md | 12 +- plan/week05-project1-redis-docker.md | 225 +++++ ...thon-basics.md => week06-python-basics.md} | 18 +- 11 files changed, 1205 insertions(+), 629 deletions(-) create mode 100644 plan/rag-project.md create mode 100644 plan/week05-project1-redis-docker.md rename plan/{week08-python-basics.md => week06-python-basics.md} (91%) diff --git a/.gitignore b/.gitignore index 6225e3b..f9fe991 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ notes/private/ node_modules/ .venv/ __pycache__/ +data/ diff --git a/README.md b/README.md index a87e41d..74293fe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # zhuangang · 转岗学习仓库 -Go 全栈基础 + Python 多 Agent 应用开发的 12 周转岗计划和全部练习代码。 +16 周转岗计划和全部练习代码:Python AI 应用 / Agent 开发为主(RAG、LangGraph、MCP、评测),Go 全栈为辅。 -从 [plan/README.md](plan/README.md) 开始看:里面有总路线、每周计划文件和进度记录。 +从 [plan/README.md](plan/README.md) 开始看:里面有总路线、岗位要求对照、每周计划文件和进度记录。 diff --git a/plan/README.md b/plan/README.md index 9b00737..4fa1bb2 100644 --- a/plan/README.md +++ b/plan/README.md @@ -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)”,提交并推送。本路径正式结束。 diff --git a/plan/agent-mini-project.md b/plan/agent-mini-project.md index c4139c0..4c23b1a 100644 --- a/plan/agent-mini-project.md +++ b/plan/agent-mini-project.md @@ -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 `(打印报告)、`agent-mini list`。 - -**运行方式**:`uv run agent-mini run "..."`,然后 `uv run agent-mini show `。 - -### 第 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`,体现前端功底。 diff --git a/plan/rag-project.md b/plan/rag-project.md new file mode 100644 index 0000000..4a25559 --- /dev/null +++ b/plan/rag-project.md @@ -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 项目调用。 diff --git a/plan/week01-go-basics.md b/plan/week01-go-basics.md index 84e3f2d..c52faa1 100644 --- a/plan/week01-go-basics.md +++ b/plan/week01-go-basics.md @@ -1,6 +1,6 @@ # 转岗计划 · 第 1 周学习计划(Go 基础语法) -> 所属阶段:第 1 个月 Go 全栈基础。总纲见同目录 `README.md`。 +> 所属阶段:第一段 Go 全栈基础(第 1~5 周)。总纲见同目录 `README.md`。 目标:一周内掌握 Go 的核心语法,最后用纯标准库写出一个能跑的命令行“任务清单”小程序,为第 2 周用 Gin 写接口打基础。 diff --git a/plan/week02-gin-api.md b/plan/week02-gin-api.md index 24cab3b..eeaa6bd 100644 --- a/plan/week02-gin-api.md +++ b/plan/week02-gin-api.md @@ -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。 diff --git a/plan/week03-postgresql.md b/plan/week03-postgresql.md index 42e0b94..6fcb72a 100644 --- a/plan/week03-postgresql.md +++ b/plan/week03-postgresql.md @@ -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、看板成员权限、按状态分列的任务,前后端完整联调。 diff --git a/plan/week04-auth-project1.md b/plan/week04-auth-project1.md index ce8d3dc..7111937 100644 --- a/plan/week04-auth-project1.md +++ b/plan/week04-auth-project1.md @@ -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`。 diff --git a/plan/week05-project1-redis-docker.md b/plan/week05-project1-redis-docker.md new file mode 100644 index 0000000..5e20c4d --- /dev/null +++ b/plan/week05-project1-redis-docker.md @@ -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 是加分项。 diff --git a/plan/week08-python-basics.md b/plan/week06-python-basics.md similarity index 91% rename from plan/week08-python-basics.md rename to plan/week06-python-basics.md index 67c1015..e2be44a 100644 --- a/plan/week08-python-basics.md +++ b/plan/week06-python-basics.md @@ -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 都会直接用上。