Files
zhuangang/plan/week08-inventory-architecture.md
T

510 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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"(?<![\w/])/root/\S+"), # sensitive-ok
}
WORDS_FILE = Path("notes/private/sensitive-words.txt")
ALLOW_MARK = "sensitive-ok" # 行内写这个标记表示人工确认过,可以放行(比如文档里讲解正则的例子)
def load_words() -> 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[前端<br/>编排循环在这里?]
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 分钟口述初稿