This commit is contained in:
2026-09-22 15:42:25 +08:00
commit 8e86665096
60 changed files with 22339 additions and 0 deletions
@@ -0,0 +1,268 @@
# 阶段一 · 小点 8:软件工程与基础设施
> 所属:阶段一 前置核心基础(必备打底)
> 定位:Agent 不是"只要模型会答就行"的玩具,它是要落地、要被人 review、要跑几个月不出乱的工程。这一节补上 Git、API、容器、数据库这四根"工程地基"。
## 精简大纲
1. 版本控制:Git 进阶(rebase / cherry-pick)、分支流规范
2. API 开发:FastAPI、RESTful、SSE 流式、WebSocket
3. 容器化:Docker、Dockerfile 分层优化、Docker Compose
4. 数据库基础:PostgreSQL / MySQL / Redis / MongoDB
## 学习内容详情
### 1. 版本控制(Git 进阶)
#### 1.1 merge 与 rebase 的区别
```mermaid
graph LR
A[功能分支开发完] --> B{合并方式}
B -->|merge| C[产生一个新的合并提交, 历史有分叉]
B -->|rebase| D[把提交搬到主分支顶端, 历史线性更清爽]
```
- **rebase(变基)**:把当前分支提交"搬到"目标分支顶端,保持线性历史;merge 产生分叉。
- **cherry-pick**:摘取某一个 commit 到当前分支——线上 bug 修在 dev 后,只把修复提交摘给 prod,不把一堆无关提交带上。
```bash
# cherry-pick 典型场景
git checkout prod # 切到生产分支
git cherry-pick <commit-hash> # 只把某个修复提交应用过来
git push origin prod
```
- **分支流(dev / test / prod)**:环境与分支一一对应;**分支保护规则(禁直推 prod)**是协作底线。
```
开发流程:
feature/x ──merge──▶ dev ──实测──▶ test ──发布──▶ prod
你自己 CI/CD 自动 (受保护, 禁直推)
```
### 2. FastAPI API 开发
#### 2.1 为什么 Agent 服务要用 FastAPI
```mermaid
graph LR
A[Agent 服务] --> B[POST /chat 普通问答]
A --> C[GET /tasks/id 查任务状态]
A --> D[GET /chat/stream SSE 流式]
A --> E[WS /realtime WebSocket 双向]
B --> F[RESTful 资源风格]
D --> G[单向输出·逐token]
E --> H[双向·语音/协作编辑]
```
- **RESTful**:以资源为中心的接口风格;对照 Agent 服务:POST /chat、GET /tasks/{id}。
- **路由**:按资源分文件组织,别全堆在 main.py。
- **依赖注入(DI)**:`Depends` 机制,全局共享资源(LLM client / 存储连接),测试时可替换成 mock。
- **SSE vs WebSocket 选型**:单向输出(Agent 逐 token 回答)用 **SSE**;双向实时交互(语音 / 协作编辑)才用 **WebSocket**。绝大多数 Agent 场景 SSE 就够。
> 🔬 **深度理解 · SSE 与 WebSocket(流式传输选型)**:
> 1. **本质**:SSE(Server-Sent Events)是"服务端 → 客户端"的单向文本事件流,基于 HTTP 长连接;WebSocket 是"双向全双工"的长连接帧协议,两者满足不同需求。
> 2. **机制**:SSE 复用 HTTP,服务端持续写 `data: ...\n\n` 事件,天然易解析、支持自动重连、无跨域握手改造成本;WebSocket 需先握手升级(http→ws)再双向收发文本/二进制帧,连接更"重"但能力更强。
> 3. **为什么重要**:Agent 逐 token 打字机输出是"单向推送给用户",SSE 恰好够用且实现最简;只有语音实时对话、协作编辑等需要"双向实时"才上 WebSocket。选错会白白增加复杂度。
> 4. **易错点**:SSE 是单向的(客户端不能跟服务器并行上行对流),需要双向就别硬套 SSE;WebSocket 要做心跳保活与断线重连,生产成本更高。
```python
# main.py —— 一个带 SSE 流式的 Agent 接口
import asyncio
import json
from fastapi import FastAPI, Depends
from fastapi.responses import StreamingResponse
app = FastAPI(title="Agent 服务")
# 依赖注入: 全局共享资源(例: LLM client), 可在测试时替换成 mock
def get_llm_client():
# 返回一个模型客户端(这里简化)
return {"model": "gpt-4o"}
@app.post("/chat")
async def chat(payload: dict, client: dict = Depends(get_llm_client)):
# RESTful: 资源是"聊天", 方法是 POST
return {"reply": "这是普通(非流式)回复"}
@app.get("/chat/stream")
async def stream_chat(llm=Depends(get_llm_client)):
"""SSE 流式接口: 逐 token 推给前端打字机渲染"""
async def event_stream():
for word in ["你好", ",", "我是", "AI", "助手"]:
yield f"data: {json.dumps({'token': word}, ensure_ascii=False)}\n\n"
await asyncio.sleep(0.1) # 模拟模型逐字生成
yield "data: [DONE]\n\n" # 结束标记
return StreamingResponse(event_stream(), media_type="text/event-stream")
```
- **响应模型**:用 `response_model=ChatResponse` 约束输出结构,自动生成 swagger 文档;请求非法自动返回 422。
```python
from pydantic import BaseModel
class ChatResponse(BaseModel):
reply: str
used_tokens: int
@app.post("/chat2", response_model=ChatResponse)
async def chat2(payload: dict):
return ChatResponse(reply="结构化响应", used_tokens=123)
```
- **运行**:`uvicorn main:app --reload --port 8000`。
### 3. 容器化(Docker / Compose)
#### 3.1 镜像分层:把"装依赖"放"拷代码"之前
```mermaid
graph LR
A[FROM python:3.12] --> B[COPY requirements 先拷贝依赖清单]
B --> C[RUN pip install 安装依赖 → 形成缓存层]
C --> D[COPY . 最后才拷源码]
D --> E[改代码时常命中缓存, 不用重装依赖 → 更小更快]
```
- **Dockerfile 多阶段构建**:先装依赖(命中缓存层)再拷源码,镜像更小。
- **镜像分层**:把"装依赖"放"拷代码"之前,改代码时命中缓存不必重装依赖。
- **docker-compose.yml**:本地一键拉起"应用 + Redis + 向量库"整套环境。
- ⚠️ 不用 `docker commit` 造镜像(不可复现),一律用 Dockerfile / Compose 声明式定义。
> 🔬 **深度理解 · Docker 镜像分层与构建缓存**:
> 1. **本质**:镜像由若干只读层叠加而成,每条 `RUN/COPY/ADD` 指令生成一层(含该层的内容 diff 与元数据);构建时若某层输入未变,直接复用缓存而不重算。
> 2. **机制**:Docker 以"指令内容 + 当前父层镜像内容"作为缓存键。先 `COPY requirements.txt` + `RUN pip install`,再 `COPY .`——这样改业务代码时依赖层缓存命中、装依赖被跳过,迭代构建又快又省。
> 3. **为什么重要**:依赖安装往往最耗时(网络拉取 / 编译),把这层前移可让"改代码→重建"的迭代秒级完成;层组织也直接影响镜像体积与传输成本。
> 4. **易错点**:若把 `COPY .` 放到前面,任何源码改动都会让后续所有层缓存失效、依赖被迫重装;`docker commit` 生成的是基于运行态的黑盒镜像、不可复现,故生产一律用 Dockerfile 声明式构建。
```dockerfile
# Dockerfile —— 多阶段构建, 镜像更小更安全
FROM python:3.12-slim AS base
WORKDIR /app
# ① 先拷依赖清单并安装 → 命中缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ② 最后才拷源码 → 改代码不动前面的缓存层
COPY . .
# 非流式/流式都从这里启
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
```yaml
# docker-compose.yml —— 一键拉起整套依赖
# version: "3.9" 新版 Compose 已弃用/忽略该字段, 省略即可, 此处仅注释说明
services:
app:
build: .
ports: ["8000:8000"]
environment:
REDIS_URL: redis://redis:6379
VECTOR_HOST: qdrant:6333
depends_on: # 等 redis/向量库起来再启动应用
- redis
redis:
image: redis:7
qdrant:
image: qdrant/qdrant # 向量数据库示例
```
### 4. 数据库基础
#### 4.1 选型速查
| 场景 | 数据库 | 关键点 |
|-|-|-|
| 结构化业务数据 / 任务元数据 | PostgreSQL / MySQL | SQL、表设计、事务 |
| 会话缓存 / 临时状态 | Redis | 内存 + TTL 过期 |
| 非结构化记忆 / 对话历史 | MongoDB | 文档存储、灵活 schema |
> ⏳ **短期可不深究**:MongoDB 的文档存储、灵活 schema、聚合管道等细节属于"非结构化记忆"这条补充线的进阶内容,第一遍只需记住"结构化业务用 SQL 库、临时缓存用 Redis、对话/记忆可用文档库"的分工即可,真接 MongoDB 时再深学。
```python
# Redis TTL: 会话 30 分钟无活动自动过期, 清理僵尸会话
import redis
r = redis.Redis(host="localhost", port=6379)
r.setex("session:abc123", timeout=1800, value="会话数据")
# ↑ setex: 设一个 1800 秒(30分钟)后自动过期的 key
print(r.get("session:abc123")) # 存在期内能读到
# 30 分钟后: r.get(...) 返回 None, 已被自动清理
```
```python
# PostgreSQL 事务: "写任务+写明细"两张表必须包事务, 要么全成要么全回滚
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
engine = create_engine("postgresql://user:pwd@localhost/agentdb")
Session = sessionmaker(bind=engine)
db = Session()
try:
db.execute(text("INSERT INTO tasks(...) VALUES(...)")) # 1) 写主任务(SQLAlchemy 2.x 裸 SQL 须用 text() 包裹)
db.execute(text("INSERT INTO task_items(...) VALUES(...)")) # 2) 写明细
db.commit() # 两步都成功才一起提交
except Exception:
db.rollback() # 任一步失败, 全部回滚, 不留半截脏数据
raise
finally:
db.close()
```
> **一句话原则**:凡是涉及"要一起变才有效"的多条写入(如任务 + 明细、账户 + 流水),必须包事务。Redis 适合可重建的临时缓存,不适合存需要强一致的核心业务。
> 🔬 **深度理解 · 数据库事务(ACID 与原子性)**:
> 1. **本质**:事务把"多条要一起成功才有效"的写入打包成原子操作——要么全提交,要么全回滚,杜绝"只写了一半"的中间态脏数据。
> 2. **机制**:围绕 ACID(原子性 / 一致性 / 隔离性 / 持久性)。实现上靠日志与锁 / 多版本并发控制(MVCC):出错时按 undo 日志回滚已写部分;并发事务用隔离级别防止读到彼此的中间状态。
> 3. **为什么重要**:Agent 的"任务 + 明细"、"账户 + 流水"这类强关联多写,少了事务会出现半截数据、账实不符;这正是关系库相对内存缓存的核心价值。
> 4. **易错点**:写操作在 `commit` 前一旦中途异常必须 `rollback`(别忘了 finally 里关闭会话);隔离级别选错会引入脏读 / 不可重复读;事务别包过长的耗时调用,长时间持锁会拖垮并发吞吐。
## 本节自检
- [ ] 能用 FastAPI 实现一个带 SSE 流式的接口并跑通
- [ ] 能编写多阶段 Dockerfile 并 compose 拉起应用 + Redis + 向量库
- [ ] 会用 Redis TTL 与 PostgreSQL 事务
- [ ] 能说清 merge vs rebase、以及何时用 cherry-pick
## 本节配套思考题(快速入门的检验)
1. merge 和 rebase 各产生什么形态的提交历史?在多人协作的共享分支上你会更偏好哪种?
2. "逐 token 回答"用 SSE,"语音实时对话"用 WebSocket,判断依据是什么?
3. 为什么 Dockerfile 要"先拷依赖再拷源码"?顺序反了会损失什么?
4. 任务表和它对应的明细表为什么要统一包在一个事务里?
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:merge 和 rebase 各产生什么形态的提交历史?在多人共享分支上你会更偏好哪种?
- **面试官想考察**:是否理解 Git 两种整合方式的语义差异与协作风险,而非只背名词。
- **专业作答(含深度)**:
1. **merge**:把另一分支并入当前,生成一个"合并提交",保留双方分叉点,历史呈网状、保留真实发生过的并行路径。
2. **rebase**:把当前分支的提交"搬到"目标分支顶端并重写这些提交(新的哈希),历史线性清爽,便于 review 与主分支快进。
3. **取舍**:rebase/GitLab merge 更整洁但**改写了提交哈希**,在已共享/已推送的分支上 rebase 会破坏他人历史引用,造成混乱;merge 更安全保守但历史分叉多。
4. **实践建议**:主分支(dev/test/prod)与多人共享分支上不要随意 rebase、用 merge 保护历史;个人功能分支(feature)在合入前可用 rebase 压平到最新主线以保持整洁。
- **加分亮点 / 深度追问**:可提"rebase 与 merge 时冲突解决的差异(rebase 逐个提交重放、冲突也要逐个解)",以及 `git pull --rebase` 与 `rerere`(自动记录冲突解法)等进阶技巧。
### 面试题 2:"逐 token 回答"用 SSE、"语音实时对话"用 WebSocket,判断依据是什么?
- **面试官想考察**:能否把"单向 vs 双向"这一本质应用到传输选型,而非背协议名。
- **专业作答(含深度)**:
1. **本质区分**:SSE 是服务端→客户端的单向事件流(基于 HTTP);WebSocket 是双向全双工长连接。
2. **判断依据**:先问"这个场景需不需要客户端在连接期主动上行?"——Agent 打字机只需服务端单向推送 → SSE 够用且简单;语音对话/协作编辑要双向实时收发 → WebSocket。
3. **成本对比**:SSE 复用 HTTP、易解析、自动重连、实现轻;WebSocket 要握手升级、心跳保活、断线重连,生产成本更高,只在确有双向需求时用。
4. **结论**:绝大多数 Agent 场景是"单向推 token",SSE 是性价比最优解。
- **加分亮点 / 深度追问**:可提"SSE 也能在一条长连接上携带多字段事件(结构化事件名 + data)",以及 WebSocket 上的二进制流(语音帧)为何比文本流更适合实时交互的细节。
### 面试题 3:为什么 Dockerfile 要先"拷依赖再拷源码"?把顺序反过来会损失什么?
- **面试官想考察**:是否理解镜像分层缓存机制,能否推导出"层顺序影响构建缓存命中"。
- **专业作答(含深度)**:
1. **分层缓存原理**:每层以"指令 + 父层内容"做缓存键,命中即复用。
2. **先装依赖**:依赖清单(requirements)变动频率远低于源码,先 `COPY requirements` + `RUN pip install` 形成稳定的依赖层;改源码只重建后面的层,秒级迭代。
3. **反序的损失**:若先 `COPY .`,任何一行源码改动都会让整个缓存失效,依赖被迫全部重装(最耗时部分白跑),构建极其缓慢。
4. **延伸**:这也说明应把"爱变的内容放后面、稳定内容放前面",并避免用 `docker commit` 这类不可复现的黑盒方式造镜像。
- **加分亮点 / 深度追问**:可提 `.dockerignore`(排除 node_modules/.git 等避免无效缓存失效)、多阶段构建(仅把运行时依赖带入最终镜像压缩体积)这两个进阶优化。