03 · 后端:FastAPI 与分层架构

一、为什么选 FastAPI

三个理由:

  1. 原生 async + SSE :Agent 要流式输出,FastAPI 的 StreamingResponse 最好用
  2. 自带 OpenAPI :/docs 直接能调试,面试演示很方便
  3. Pydantic 校验:请求体自动校验,省一堆 if

二、目录即架构

bash 复制代码
app/
├── config.py               # 配置中心(唯一读环境变量的地方)
├── llm/
│   ├── providers.py        # 模型适配:重试 + 降级 + 流式 + Mock
│   └── prompts.py          # Prompt 模板(版本化管理)
├── rag/
│   ├── loader.py           # 文档切分
│   ├── embedding.py        # 向量化
│   ├── vectorstore.py      # 向量库(memory / pgvector / chroma)
│   └── retriever.py        # 检索编排
├── agent/
│   ├── state.py            # AgentState
│   ├── graph.py            # 编排
│   ├── tools.py            # 工具
│   ├── guardrails.py       # 护栏
│   └── memory.py           # 记忆
├── observability/tracer.py # 可观测
├── api/routes.py
└── main.py

分层原则 :llm / rag / agent 三层彼此不知道 HTTP 的存在, api 层只做协议转换。好处是------换成 Java 实现时,业务逻辑是一模一样的(见第 09 篇)。

三、配置中心:一处读环境变量

python 复制代码
# app/config.py
@dataclass
class Settings:
    llm_provider: str = "openai"
    llm_base_url: str = "https://api.deepseek.com/v1"
    ...
    @classmethod
    def from_env(cls) -> "Settings": ...

settings = Settings.from_env()

三个细节:

  1. 自动加载 .env:
python 复制代码
from dotenv import load_dotenv
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
load_dotenv(os.path.join(_BASE_DIR, ".env"), override=False)
  1. 自动降级 :没配 Key 时 llm_provider 自动从 openai 变 mock, embed_provider 自动从 openai 变 hashing------保证永远不会因为缺配置而启动失败。

  2. 全环境变量可覆盖:Docker / K8s 里直接注入 env,不用改代码。

四、LLM 适配层:屏蔽厂商差异

python 复制代码
class BaseLLM:
    def chat(self, messages, *, temperature=None, json_mode=False, trace_id="", span_name="llm"): ...
    def stream(self, messages, *, temperature=None, trace_id=""): ...

OpenAICompatibleLLM 走 OpenAI SDK,DeepSeek / Qwen / 通义 / vLLM / Ollama 全兼容 (它们都实现了 /v1/chat/completions)。

重试与主备降级

python 复制代码
candidates = [self.model] + self.fallback_models   # 主模型 + 备用模型
for model in candidates:
    for attempt in range(self.max_retries + 1):    # 指数退避 0.6s / 1.2s
        try:
            return self._client.chat.completions.create(**kwargs)
        except Exception as e:
            if attempt < self.max_retries:
                time.sleep(0.6 * (2 ** attempt))

为什么要主备降级 :线上遇到过主模型限流/故障,整个 Agent 就不可用了。 配一个 LLM_FALLBACK_MODELS=qwen-plus,deepseek-chat 就能自动切。

Token 统计

每次调用把 usage 上报 Tracer:

python 复制代码
tracer.record_llm(trace_id, u["prompt"], u["completion"], model=resp.model)

前端右上角实时显示累计 token------成本可见,才谈得上治理。

五、Prompt 模板:五段式 + 集中管理

python 复制代码
PLANNER_TMPL = """{system_role}

【可用工具】
{tools}

【历史对话摘要】
{summary}

【用户问题】
{question}

【任务】
把问题拆解为 1~3 个可执行步骤......严格输出 JSON......"
"""

我把所有模板放在 llm/prompts.py,理由:

  • 可版本化:改 prompt 不改业务代码,能灰度、能 A/B
  • 可测试:把模板抽出来后,能对同一模板做回归
  • 可复用:Python 侧和 Java 侧用的是同一套模板文本

五段式结构:角色 → 目标 → 约束 → 输出格式 → 少样本示例。 规划节点强制输出 JSON,并配了容错解析器(去 markdown 代码块、截取首个 JSON 对象):

python 复制代码
def parse_json_safe(text):
    m = re.search(r"```(?:json)?\s*(.*?)```", text, re.S)  # 去代码块
    ...
    start, end = raw.find("{"), raw.rfind("}")              # 截取 JSON

模型偶尔会输出 json ... ,这个解析器救过很多次。

六、API 设计

接口 方法 说明
/api/chat POST SSE 流式对话
/api/chat/sync POST 非流式(curl / 定时任务 / 单测)
/api/knowledge/ingest POST 知识库重建索引
/api/knowledge/search POST 检索调试
/api/traces、/api/traces/{id} GET Trace 查询
/api/tools GET 工具清单(暴露给前端展示)
/api/sessions/{id} GET/DELETE 会话记忆

SSE 事件类型:

java 复制代码
trace_start / plan / tool_start / tool_end / reflect / hitl
token(增量文本) / llm_usage / final / error / stream_end

七、一个踩坑:同步图 + async 服务会阻塞事件循环

LangGraph 的 invoke 是同步 的,直接在 async def 里调用会把整个事件循环卡死。

解法:线程池跑图 + 线程安全回传事件

python 复制代码
def worker():
    for event in runner.run(query, sink=sink):
        loop.call_soon_threadsafe(queue.put_nowait, event)

asyncio.get_running_loop().run_in_executor(None, worker)

sink 用 loop.call_soon_threadsafe 把事件从工作线程推回主线程的 asyncio.Queue。 这样多个用户同时提问也不会互相阻塞。


下一篇:04 · RAG 全流程:从切分到引用溯源

相关推荐
爱敲代码的小黄35 分钟前
神经网络入门指南:从 CNN、RNN 到 Attention
人工智能·面试·程序员
brave102511 小时前
LLMGuide 大模型面试指南
人工智能·ai·面试·ai编程
Rain的Java大神之路15 小时前
面试官最烦的答案:“我用 Redis + Token”!接口幂等 8 种方案 + 四层防御,把重复扣款工单干到 0
java·后端·面试
Moment16 小时前
Harness Engineering 为什么是 AI Coding 的工程底座 ❓❓❓
前端·后端·面试
利威尔xu16 小时前
2026-11-08-Go-defer-panic-recover面试
面试·golang
CoderYanger1 天前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第二弹)
java·数据库·spring boot·spring·面试·java-ee·mybatis
wo_jack1 天前
C++ 入门笔记(十六):第16课 vector 模拟实现十问十答 —— 从三指针到迭代器失效
c++·后端·面试·stl·vector
聪明的笨猪猪2 天前
AI应用架构选型:架构师的核心决策困境与权衡
经验分享·面试·架构
疯华整猫2 天前
被杀掉之后:我把 Android 的「唤起 + 常驻」重做了一遍
前端·面试·架构
nice_lcj5202 天前
Java 并发容器入门:ConcurrentHashMap、CopyOnWriteArrayList、阻塞队列
java·面试·并发编程·阻塞队列·concurrenthashmap·copyonwritearraylist