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 全流程:从切分到引用溯源

相关推荐
此时不提桶,更待何时10 小时前
05-03-B-ClickHouse与OLAP面试与生产事故实战
clickhouse·面试·nosql
此时不提桶,更待何时11 小时前
03-04-B-连接池与DB治理面试与生产事故实战
mysql·面试
kaiyou202612 小时前
数据分析岗面试,如何把考证学到的知识讲成业务案例?
面试·数据挖掘·数据分析
智购科技自动售货机工厂16 小时前
数字人民币硬钱包支付失败,排查发现是NFC读卡器功率不足~YH
python·面试·架构·eclipse·emacs
Sam_Deep_Thinking18 小时前
如何理解java的信号量
java·后端·面试·程序员
王中阳Go19 小时前
面试官问"你怎么证明它有效",200个转AI的后端没几个答得上来
人工智能·后端·面试
进击的明明19 小时前
TypeScript速通笔记(上)
前端·面试·typescript
老马识码19 小时前
[agent开发面试]上下文工程与压缩:Agent 的稀缺资源
人工智能·面试
时间的拾荒人20 小时前
Qt 信号与槽机制详解(二):连接方式详解与面试指南
开发语言·qt·面试
我叫黑大帅1 天前
Go日志库工程选型与逃逸分析评测报告
后端·面试·go