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 自动从 openaimockembed_provider 自动从 openaihashing------保证永远不会因为缺配置而启动失败

  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)

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


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

相关推荐
Interview Aid1125 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
ocean21039 小时前
2025-2026年AI提效与实践大厂面试高频问题
人工智能·面试·职场和发展·提示词工程·ai提效
程序员梅雨10 小时前
Linux & Shell 实用干货
linux·运维·服务器·后端·面试·php
YonyouHRSaaS13 小时前
AI视频面试系统定义、功能作用、品牌推荐、选择攻略
人工智能·面试·职场和发展·ai面试·视频面试·ai视频面试
小鱼爱吃草灬灬14 小时前
实时面试辅助排查清单:音频来源、问题输入与上下文
面试·职场和发展·音视频
颜挺锐17 小时前
如何轻松通过性能测试面试之第九篇:RPO、RTO是什么意思
面试·职场和发展
李剑一17 小时前
大环境或许真的恶劣了起来,打工人你焦虑吗?
面试·程序员·招聘
Rain的Java大神之路18 小时前
手写一个Spring容器
java·后端·mysql·spring·面试
艾莉丝努力练剑18 小时前
【AI大模型接入SDK】Ollama本地大语言模型部署
c++·人工智能·语言模型·自然语言处理·面试