--- id: plan.week08 title: "第 8 周:阶段 0 盘点 + 阶段 1 读通与画图" type: week domain: plan stack: [git] week: [8] est_hours: 12 order: 8 --- # 第 8 周:阶段 0 盘点 + 阶段 1 读通与画图 > 第三段“Agent 主线”的第 1 周。上周 `week07-frontend.md`,下周 `week09-case-cards.md`。 **为什么现在做**:你在公司做的多 Agent 产品是**最有说服力的经历**,但面试官要听的是“机制和取舍”,不是“我改过某个页面”。这周把公司里的真实链路读通、画清楚,下周提炼成案例卡,第 10~13 周的项目三就有了明确的设计输入。 **本周规则** - 读公司代码在**公司电脑**上做;仓库里的笔记**只写机制,不写**真实路径、接口地址、表名、字段名、内网地址、密钥、客户数据。私有细节写在 `notes/private/`(已在 `.gitignore`)。 - 每天产出至少一个文件。 - 模块一律用**通用叫法**:比如“前端多 Agent 研讨模块”“Agent 网关的作业接口”“草稿存储”。 **本周产出** ```text notes/ ├── 00-inventory.md # 阶段 0:目标、模块清单、A~E 自评 ├── 01-architecture.md # 阶段 1:架构图(mermaid)+ 时序图 + 断点标注 ├── 01-api-table.md # 阶段 1:关键接口职责表(通用叫法) ├── 01-state-machine.md # 阶段 1:流程的状态机草图 ├── 01-pitch-draft.md # 3 分钟口述初稿 └── private/ # 不进仓库 tools/ └── check_sensitive.py # 提交前的脱敏检查脚本 ``` --- ## 第 1 天:阶段 0——盘点 ### 学习内容 **1. 一句话目标** - 说明:写下来,后面每个取舍都拿它来对照。 - 案例: ```markdown ## 一句话目标 16 周后,以“有多 Agent 产品落地经验的 AI 应用工程师”身份,投递 AI Agent 开发 / 大模型应用开发 / AI 全栈岗位; 主语言 Python,加分项 Go 全栈 + 7 年前端。 ``` **2. 模块清单** - 说明:列出你在公司**亲自改过、排查过**的模块,只写通用名字和你做了什么。这份清单决定下周写哪些案例卡。 - 案例(示例格式,按你的实际情况填): ```markdown | 模块(通用叫法) | 我做过什么 | 遇到过的问题 | 对应重点 | | --- | --- | --- | --- | | 前端多 Agent 研讨模块 | 实现多角色轮流发言的界面和流式渲染 | 关闭页面后研讨中断 | A、C | | 前端会话与交付模块 | 交付结果展示、刷新后恢复 | 刷新后进度丢失 | B | | Agent 网关的作业接口(联调) | 对接创建作业、查询进度 | 状态没有回写到任务 | B、E | | 模型配置 / 切换 | 额度不足时的报错与切换 | 换模型后流程无法继续 | D | ``` **3. A~E 自评表** - 说明:五个学习重点贯穿第 8~13 周。先诚实地评估自己现在的水平。 | # | 重点 | 一句话解释 | 学完应能回答 | | --- | --- | --- | --- | | A | 多 Agent 编排 | 多个角色(提议者、评审者、总结者……)由谁派活、按什么顺序、什么时候结束 | “角色怎么分工、什么时候结束、怎么避免同一个任务被跑两次” | | B | 状态与权威源 | 同一份数据(进度、结果)如果存了多处,以哪一处为准 | “刷新或关页后为什么丢进度,权威数据写在哪” | | C | 流式与长任务 | 结果边生成边推送;任务很长时,不能依赖一条连接一直不断 | “为什么编排不该只活在浏览器里” | | D | 人机协作 | 流程中途需要用户确认、补充信息、换模型后继续 | “Agent 什么时候必须停下来问人” | | E | 与业务系统集成 | 模型的输出最终要变成可审计的业务记录(任务、报告) | “LLM 的结果怎样落到可审计的业务对象上” | ```markdown | 重点 | 已会 | 模糊 | 不会 | | --- | --- | --- | --- | | A 编排 | 前端按顺序调用各角色 | 结束条件由谁判断 | 服务端编排框架 | | B 权威源 | | 草稿和任务表的关系 | | | C 流式 | SSE 渲染 | 断线后服务端在做什么 | 后台作业、续跑 | | D 人机协作 | | 澄清问题的触发条件 | interrupt / 恢复 | | E 集成 | | 过程报告怎么生成 | 幂等写入 | ``` ### 要建的文件 - `notes/00-inventory.md`:一句话目标、模块清单、A~E 自评、项目三技术栈确认(Python + LangGraph,先手写循环) ### 运行方式 - 无代码。写完后自己读一遍:每一行是否都**不含**公司的真实名称和路径。 ### 完成标准 - [ ] 一句话目标已写下 - [ ] 模块清单至少 3 行 - [ ] A~E 三列填完;项目三技术栈已确认 --- ## 第 2 天:脱敏机制——提交前自动检查 ### 学习内容 - 说明:靠“记得小心”是不够的,要有机制。写一个脚本,在每次提交前扫描**即将提交的内容**,发现内网地址、密钥格式、你自定义的敏感词就拒绝提交。敏感词清单本身是敏感的,放在 `notes/private/sensitive-words.txt`(不进仓库)。再用 Git 的 `pre-commit` 钩子自动运行。 - 案例: ```python # tools/check_sensitive.py —— 提交前扫描暂存区内容 # 不自动运行:作为 pre-commit 钩子在仓库根目录执行;加 --self-test 可以单独自检 import re import subprocess import sys from pathlib import Path PATTERNS = { "内网 IP": re.compile(r"\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|\b192\.168\.\d{1,3}\.\d{1,3}\b|\b172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}\b"), "疑似密钥": re.compile(r"(?i)\b(?:sk|ak|api[_-]?key|secret|token)[\w-]*\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}"), "私钥": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), "root 绝对路径": re.compile(r"(? list[str]: if not WORDS_FILE.exists(): return [] return [w.strip() for w in WORDS_FILE.read_text(encoding="utf-8").splitlines() if w.strip() and not w.startswith("#")] def scan_text(name: str, text: str, words: list[str]) -> list[str]: problems = [] for no, line in enumerate(text.splitlines(), start=1): if ALLOW_MARK in line: continue for label, pat in PATTERNS.items(): if pat.search(line): problems.append(f"{name}:{no} {label}") for w in words: if w.lower() in line.lower(): problems.append(f"{name}:{no} 敏感词(见私有清单)") # 不打印敏感词本身 return problems def staged_files() -> list[str]: out = subprocess.run(["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"], capture_output=True, text=True, check=True).stdout return [f for f in out.splitlines() if f] def main() -> int: words = load_words() problems = [] for f in staged_files(): content = subprocess.run(["git", "show", f":{f}"], capture_output=True).stdout # 读暂存区版本 try: problems += scan_text(f, content.decode("utf-8"), words) except UnicodeDecodeError: continue # 跳过二进制文件 for p in problems: print("✗", p) if problems: print("发现疑似敏感内容,已阻止提交。确认无误的行可加注释 sensitive-ok。") return 1 if problems else 0 if __name__ == "__main__" and "--self-test" in sys.argv: sample = "url: http://192.168.1.20:8080\napi_key = 'abcdefghijklmnopqrstuvwxyz'\n正常的一行\n" # sensitive-ok found = scan_text("demo.md", sample, ["某客户"]) assert len(found) == 2, found assert scan_text("demo.md", "某客户的需求", ["某客户"]) == ["demo.md:1 敏感词(见私有清单)"] print("自检通过:", found) elif __name__ == "__main__": sys.exit(main()) ``` 把它挂到 Git 钩子上(钩子文件不会被提交,换设备后要重新执行一次): ```bash cat > .git/hooks/pre-commit <<'HOOK' #!/bin/sh exec python3 tools/check_sensitive.py HOOK chmod +x .git/hooks/pre-commit python3 tools/check_sensitive.py --self-test ``` **易错点 / 面试常问(补充)** - 易错点:钩子文件在 `.git/hooks/` 里,不会被提交,换电脑要重新安装(总纲第 12 节有步骤)。 - 易错点:敏感词清单本身也是敏感信息,只放 `notes/private/`;检查脚本误报时用允许标记,不要直接关掉钩子。 - 易错点:已经提交过的敏感内容,删掉文件后**仍然在 Git 历史里**;一旦推送过,要当作已经泄露处理(例如密钥立即作废),再考虑改写历史。 ### 要建的文件 - `tools/check_sensitive.py`;`notes/private/sensitive-words.txt`(公司名、产品代号、内部项目名、同事名等,**不进仓库**);`.git/hooks/pre-commit` ### 运行方式 ```bash python3 tools/check_sensitive.py --self-test echo "测试 192.168.0.1" > notes/x.md # sensitive-ok git add notes/x.md && git commit -m test # 应被拒绝 git restore --staged notes/x.md && rm notes/x.md # 清理 ``` ### 完成标准 - 自检通过;故意提交含内网 IP 的文件会被拦下。 - 在 README 的“换设备”步骤里记一笔:克隆后要重新安装钩子。 --- ## 第 3 天:阶段 1——怎么读一个大项目 ### 学习内容 - 说明:不要从第一个文件读到最后一个。方法是**跟着一次请求走**: 1. 找入口:前端从“用户点击开始研讨”的按钮事件开始;后端从路由表开始。 2. 跟调用链:每一层只记“谁调谁、传了什么、状态存在哪”,不看细节实现。 3. 搜关键词:`grep -rn "stream\|sse\|EventSource"`、`"checkpoint\|job\|draft"`,快速定位流式和状态相关代码。 4. 打日志 / 打断点:在开发环境跑一遍,用浏览器 Network 面板看实际的请求顺序。 5. 边读边画时序图,读完一块画一块。 - 案例:用 mermaid 时序图记录调用链(下面是**通用示例**,按你读到的实际情况改,名字保持通用): ```mermaid sequenceDiagram participant U as 用户 participant F as 前端 participant G as Agent 网关 participant L as 大模型 participant S as 草稿/作业存储 U->>F: 输入需求,点击开始 F->>G: 意图识别 G->>L: 调用模型 L-->>G: 意图结果 G-->>F: 返回意图 loop 每一轮研讨(由谁驱动?前端还是网关?) F->>G: 让某个角色发言(SSE) G->>L: 流式调用 L-->>G: token 流 G-->>F: SSE 推送 F->>S: 保存草稿(谁写?什么时候写?) end F->>G: 生成总结 / 交付 ``` - 读的时候在图上标问号的地方,就是第 9 周案例卡的素材。 ### 要建的文件 - `notes/01-architecture.md`:先放时序图;`notes/private/reading-log.md`:真实文件路径、函数名(不进仓库) ### 运行方式 - 在公司电脑上按上面的方法读 P0 模块: | 优先级 | 要读的模块(通用叫法) | 要搞清的问题 | | --- | --- | --- | | P0 | 前端多 Agent 功能:意图、多席研讨、草稿、作业 | 几个步骤怎么串起来,谁发起 SSE,谁驱动下一轮 | | P0 | 前端会话与交付模块 | 会话状态和交付状态分别存在哪 | | P0 | Agent 网关里多 Agent、意图、作业、草稿相关的接口 | 服务端是“单轮调用”还是“后台作业”;用了什么框架、检查点存在哪 | | P1 | 前端对 Agent 网关的接口契约 | 前后端约定了哪些接口 | | P2 | 团队里关于“后台作业”的设计文档(如果有) | 理想架构和现状差在哪 | ### 完成标准 - 时序图覆盖一次完整流程;标出至少 3 个“问号”。 --- ## 第 4 天:画架构图,标出断点 ### 学习内容 - 说明:时序图讲“顺序”,架构图讲“组成和数据流向”。这张图要回答三个问题: 1. 编排(决定下一步做什么)跑在哪一层? 2. 每类状态(会话、草稿、作业进度、业务任务)的**权威数据**存在哪? 3. **关掉浏览器时,流程断在哪一层?** - 案例(通用示例,用颜色标出风险点): ```mermaid flowchart LR U[用户浏览器] -->|SSE| FE[前端
编排循环在这里?] FE -->|HTTP| GW[Agent 网关] GW --> LLM[大模型服务] GW --> D[(草稿存储)] GW --> J[(作业表)] GW --> B[(业务库:任务/报告)] FE -.->|直接写草稿?| D classDef risk fill:#ffe0e0,stroke:#d33 class FE risk ``` ```markdown ### 断点分析 - 关闭浏览器 → 前端的编排循环停止 → 正在进行的这一轮在网关里可能继续跑完,但**不会有下一轮**。 - 刷新页面 → 前端内存里的进度丢失 → 只能从草稿恢复,而草稿可能不是最新的。 - 结论:编排如果只活在浏览器里,任务的生命周期就等于标签页的生命周期。 ### 权威源 | 状态 | 存在哪 | 谁写 | 是否权威 | 问题 | | --- | --- | --- | --- | --- | | 研讨进度 | 前端内存 + 草稿 | 前端 | 否 | 刷新丢失 | | 作业状态 | 作业表 | 网关 | 是 | 没回写业务任务 | | 业务任务状态 | 业务库 | ? | 应该是 | 显示“未开始” | ``` ### 要建的文件 - `notes/01-architecture.md`:补架构图 + 断点分析 + 权威源表 ### 完成标准 - [ ] 图上标出“关掉浏览器时流程断在哪一层” - [ ] 指出至少一处“状态双写或权威来源不清”的位置 --- ## 第 5 天:接口职责表 ### 学习内容 - 说明:把前后端约定的关键接口按**职责**整理(不写真实路径)。这张表是你第 10~12 周设计项目三接口的参照,面试时也能用来讲“我是怎么划分接口的”。 - 案例: ```markdown | 职责(通用叫法) | 方法 | 输入 | 输出 | 同步/流式/异步 | 幂等吗 | 失败时前端怎么办 | | --- | --- | --- | --- | --- | --- | --- | | 初始化研讨 | POST | 需求、角色配置 | 会话 ID | 同步 | 否 | 提示重试 | | 运行一轮并流式返回 | POST | 会话 ID、角色 | token 流 | 流式(SSE) | 否 | 断线后无法续上 | | 保存 / 读取草稿 | PUT/GET | 会话 ID、内容 | 草稿 | 同步 | 是(覆盖写) | — | | 创建后台作业 | POST | 会话 ID | 作业 ID | 异步 | ? | 轮询状态 | | 查询作业进度 | GET | 作业 ID | 状态、进度 | 同步 | 是 | — | ``` - 面试常问:“这个接口为什么设计成流式 / 异步?”“重复提交会怎样?”——在表里先想清楚。 ### 要建的文件 - `notes/01-api-table.md` ### 完成标准 - 至少 5 个接口;每个都填了“幂等吗”和“失败时怎么办”。 --- ## 第 6 天:把流程画成状态机,并用代码验证思路 ### 学习内容 - 说明:多 Agent 流程本质上是一个**状态机**:有限个状态(排队、研讨中、等待用户、总结中、完成、失败)+ 明确的转移规则。把它显式写出来有三个好处: 1. 任何时刻都知道“现在在哪一步”; 2. 状态可以存进数据库,进程重启后从同一步继续(第 12 周的检查点续跑); 3. 非法转移(比如“已完成”又回到“研讨中”)能被拦下。 - 案例:一个最小的状态机,每次转移都写进 SQLite,模拟“进程崩溃后从数据库恢复”。 ```python import sqlite3 from enum import Enum class S(str, Enum): QUEUED = "queued" DISCUSSING = "discussing" WAITING_HUMAN = "waiting_human" SUMMARIZING = "summarizing" DONE = "done" FAILED = "failed" ALLOWED = { S.QUEUED: {S.DISCUSSING, S.FAILED}, S.DISCUSSING: {S.DISCUSSING, S.WAITING_HUMAN, S.SUMMARIZING, S.FAILED}, S.WAITING_HUMAN: {S.DISCUSSING, S.FAILED}, S.SUMMARIZING: {S.DONE, S.FAILED}, S.DONE: set(), S.FAILED: set(), } class Run: def __init__(self, db: sqlite3.Connection, run_id: str): self.db, self.run_id = db, run_id db.execute("CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, state TEXT, round INT)") db.execute("INSERT OR IGNORE INTO runs VALUES (?, ?, 0)", (run_id, S.QUEUED.value)) def load(self) -> tuple[S, int]: state, rnd = self.db.execute("SELECT state, round FROM runs WHERE id = ?", (self.run_id,)).fetchone() return S(state), rnd def to(self, new: S, round_: int | None = None) -> None: cur, rnd = self.load() if new not in ALLOWED[cur]: raise ValueError(f"非法转移:{cur.value} → {new.value}") with self.db: # 事务:要么都写进去,要么都不写 self.db.execute("UPDATE runs SET state = ?, round = ? WHERE id = ?", (new.value, rnd if round_ is None else round_, self.run_id)) db = sqlite3.connect(":memory:") run = Run(db, "r1") run.to(S.DISCUSSING, round_=1) run.to(S.DISCUSSING, round_=2) print("崩溃前:", run.load()) # 模拟进程崩溃:丢掉 Python 对象,只留下数据库 del run recovered = Run(db, "r1") state, rnd = recovered.load() print(f"重启后:从 {state.value} 第 {rnd} 轮继续") recovered.to(S.SUMMARIZING) recovered.to(S.DONE) try: recovered.to(S.DISCUSSING) except ValueError as e: print("被拦下:", e) ``` - 对照第 4 天的断点分析:如果公司现在的进度只在前端内存里,就相当于没有这张 `runs` 表。 **易错点 / 面试常问(补充)** - 易错点:状态机的每一步要先写“权威状态”(数据库),再做后续动作;顺序反了,崩溃时就会出现“动作做了但状态没记”的情况。 - 面试常问:为什么要把流程画成状态机(每个状态能做什么、出错后停在哪、怎么恢复,一目了然);“状态在内存”和“状态在数据库”对崩溃恢复意味着什么。 ### 要建的文件 - `notes/01-state-machine.md`:mermaid 状态图 + 上面的代码 + “公司现状 vs 状态机化之后”的对比 ```mermaid stateDiagram-v2 [*] --> queued queued --> discussing discussing --> discussing: 下一轮 discussing --> waiting_human: 需要澄清 waiting_human --> discussing: 用户回复 discussing --> summarizing: 达成共识 / 到达轮数上限 summarizing --> done queued --> failed discussing --> failed summarizing --> failed ``` ### 运行方式 ```bash python3 notes/state_machine_demo.py # 把上面的代码存成这个文件运行 ``` ### 完成标准 - 能用状态图讲清“关页面、刷新、进程崩溃”三种情况下,状态机化前后分别会怎样。 --- ## 第 7 天:阶段 1 验收 + 口述初稿 ### 学习内容 - 说明:用“架构图 + 3 分钟”讲一遍,录音回听。结构建议: 1. 背景一句话(产品做什么,不说公司名); 2. 架构:几层、各自职责; 3. 关键问题:编排在哪、状态在哪、断了怎么办; 4. 如果重做:状态机化、后台作业、单一权威源。 - 案例: ```markdown ## 3 分钟口述初稿 我参与的是一个多 Agent 研讨产品:用户提出需求,系统先识别意图,再让提议、评审等多个角色轮流讨论,最后生成总结和交付物。 架构分三层:前端负责交互和流式展示,Agent 网关负责调用模型和保存草稿、作业,业务库保存任务和报告。 我遇到的核心问题是:研讨的“下一轮”由前端驱动,所以关掉页面流程就停了;进度一部分在前端内存、一部分在草稿里,刷新后会丢。 如果重做,我会把流程建成服务端状态机,每一步持久化,前端只订阅进度;业务任务的状态以作业表为唯一权威,由作业完成事件回写。 我在个人项目里用 LangGraph 的检查点实现了这套设计,可以演示中途杀掉进程后从断点继续。(第 12 周完成后补上) ``` ### 要建的文件 - `notes/01-pitch-draft.md`;在 README 进度表勾掉第 8 周 ### 运行方式 - 手机录音,计时;超过 3 分钟就删内容。 ### 完成标准 - [ ] 不看代码能口述多 Agent 功能的主流程 - [ ] 架构图标出了断点和权威源问题 - [ ] 口述初稿 ≤ 3 分钟 - [ ] 提交前脱敏检查脚本通过 --- ## 本周自查清单 - [ ] `notes/00-inventory.md`:目标、模块清单、A~E 自评 - [ ] `tools/check_sensitive.py` + pre-commit 钩子 - [ ] `notes/01-architecture.md`:时序图、架构图、断点、权威源 - [ ] `notes/01-api-table.md`:接口职责表 - [ ] `notes/01-state-machine.md`:状态图 + 可运行的状态机示例 - [ ] `notes/01-pitch-draft.md`:3 分钟口述初稿