📖 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

🛠 第二步:动手实操

  1. 跑通脚手架:复制 project_day11_15/ 为你的工作目录 → pip install -r requirements.txt → 配 DEEPSEEK_API_KEY → uvicorn app.main:app --reload → 浏览器开 http://127.0.0.1:8000/docs 看到 /health 正常。
  2. 读 README 的"设计约定"五条——接下来的 TODO 都在遵守它们。
  3. Day 11 TODO(按顺序做):
    • schemas.py TODO 1.1/1.2:ToolCallRecord / ErrorResponse 模型
    • llm.py:不用写,但通读一遍——它就是 tutorial_03 第 3/4 步的服务版
    • agent.py TODO 4.1:核心循环(按伪代码写,这是今天的主菜)
    • main.py TODO 5.1/5.2:/chat 路由 + 统一错误处理
  4. 自测: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 已内置,别删)

🔗 延伸资源