📖 Day 11 学习页:项目设计与核心开发(上):架构 + FastAPI
Day 11:项目设计与核心开发(上)——架构、FastAPI、Agent 核心循环
🎯 今日目标:把 Day 1-10 写过的所有"零件"装进一个分层清晰的 FastAPI 服务, 用
curl对/chat发一条消息并拿到 Agent 的回答——你的 Agent 从此是一个服务,不再是一个脚本。
🛠 项目脚手架:project_day11_15/(开发都在这个目录里) | 📋 验收清单
⏱ 今日安排(约 2 小时)
| 时间 | 内容 | 方式 |
|---|---|---|
| 30min | 理论学习:从脚本到服务 + 分层架构 | 阅读 |
| 20min | 准备:跑通脚手架(装依赖 / 配 Key / 起服务) | 操作 |
| 70min | 动手实操:完成 schemas / agent / main 的 TODO | 编码 |
📖 第一步:从脚本到服务,最关键的变化是"状态"
Day 1-10 的脚本有个隐含前提:一次运行只有一个对话,所以全局变量装对话历史就够了。
服务化之后完全不同——服务常驻运行,多个用户同时对话,张三的历史绝不能串给李四。
所以第一课:状态必须按 session_id 隔离,这就是 memory.py 存在的意义。
1. 分层架构(脚手架已经搭好,你要能说清每一层为什么存在)
project_day11_15/app/
├── main.py ← 路由层:只做 HTTP 与内部转换,保持"薄"
├── schemas.py ← 请求/响应模型(Pydantic):HTTP 层的契约
├── agent.py ← 业务层:Agent 核心循环(今天的主战场)
├── tools.py ← 工具系统(Day 12 完善)
├── memory.py ← 会话记忆(Day 12 完善)
├── llm.py ← LLM 适配层(已写好,读懂即可)——换供应商只改这一个文件
└── config.py ← 配置中心(已写好)——Key/模型名/超时 单点管理
| 分层原则 | 为什么 |
|---|---|
| 路由层薄 | main.py 只做参数校验和调用,读路由就知道全部功能;业务逻辑不藏在 HTTP 里 |
| 配置单点 | 所有 os.environ 收口在 config.py——找配置只看一个文件 |
| 供应商可替换 | DeepSeek 换 Qwen/本地模型,只动 llm.py,其他层零改动 |
| 状态按会话隔离 | memory.py 用 session_id 做键,杜绝用户间串话 |
2. FastAPI 最小可用集(今天 main.py 要用到的全部)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="我的 Agent")
class ChatRequest(BaseModel): # 请求体 = Pydantic 模型,自动校验
session_id: str = "default"
message: str
@app.post("/chat")
async def chat(req: ChatRequest):
try:
return {"answer": await 业务逻辑(req.message)}
except LLMError as e: # 业务异常 → HTTP 状态码(不泄露 traceback)
raise HTTPException(status_code=503, detail=str(e))
# 启动:uvicorn app.main:app --reload
# 测试:curl -X POST http://127.0.0.1:8000/chat -H "Content-Type: application/json" -d '{"session_id": "s1", "message": "你好"}'
# 交互式文档:http://127.0.0.1:8000/docs
3. Agent 核心循环 = 你写过的东西组装起来
run_agent(session_id, user_input):
memory = MEMORY.get(session_id) # Day 9:会话隔离的记忆
memory.add("user", user_input)
messages = [system] + memory.to_api_format()
for 轮 in range(MAX_TOOL_ROUNDS): # Day 4:有上限的 Function Calling 循环
resp = await llm.chat(messages, tools=get_tool_list())
有 tool_calls?
├─ 是 → execute_tool 执行 → 结果以 role=tool 回填 → 下一轮
└─ 否 → 最终回答 → memory.add("assistant", 回答) → return
🛠 第二步:动手实操
- 跑通脚手架:复制
project_day11_15/为你的工作目录 →pip install -r requirements.txt→ 配DEEPSEEK_API_KEY→uvicorn app.main:app --reload→ 浏览器开http://127.0.0.1:8000/docs看到 /health 正常。 - 读 README 的"设计约定"五条——接下来的 TODO 都在遵守它们。
- Day 11 TODO(按顺序做):
schemas.pyTODO 1.1/1.2:ToolCallRecord / ErrorResponse 模型llm.py:不用写,但通读一遍——它就是 tutorial_03 第 3/4 步的服务版agent.pyTODO 4.1:核心循环(按伪代码写,这是今天的主菜)main.pyTODO 5.1/5.2:/chat 路由 + 统一错误处理
- 自测:
curl打 /chat——无 Key 时应收到 503 和清晰错误说明(这也是功能!); 有 Key 时问"你好"应得到回答,问"帮我计算 3 + 5 * 2"要等 Day 12 工具系统完成后才能走工具。
✅ 第三步:验收(Day 11 部分)
- uvicorn 能启动,
/docs可见且能直接调试 GET /health返回状态POST /chat无 Key 时返回 503 + 人话错误(不裸崩、不泄露 traceback)- 有 Key 时:普通问题得到回答,且第二轮能记住第一轮说过的内容(记忆已接入)
- 代码遵守五条设计约定(自查)
- 完整清单见 验收标准文档 Day 11-15 交付清单
⚠️ 避坑
- ❌ 用全局变量装对话历史 → 多用户串台;必须 session_id 隔离(memory.py)
- ❌ 在 main.py 里写 Agent 循环 → 路由层膨胀到没法测;业务进 agent.py
- ❌ 把 LLM 的原始异常 traceback 直接返回给客户端 → 泄露内部信息;统一转 HTTPException
- ❌ 请求不设 max_tokens / 超时 → 费用和挂起风险(llm.py 已内置,别删)