去年有个后端项目差点让我崩溃——三个文件里塞着同一套提示词,某个路由里藏着十七行临时写的解析逻辑,而那个"临时方案"已经跑了八个月。
这次我换了个开场白:先问"这项目三个月后会怎么烂掉",再写第一行代码。
从Express到FastAPI:问题变了,坑没变
我之前用Node.js+Express,文件夹看着很标准:routes、controllers、services、models。灵活是灵活,但几个月后controllers胖成球,services变成垃圾场,逻辑到处复制粘贴。
FastAPI给的东西更强大,却完全不教你怎么组织。我见过太多项目最后变成这样:所有代码塞进main.py,业务逻辑糊在路由函数里,LLM调用像撒胡椒面一样散落各处。
一旦LLM进场,混乱速度翻倍。非确定性输出、提示词敏感、供应商依赖——这三件事叠加,没有结构的项目死得很快。
项目背景:不只是CRUD
这是一个真实的房产咨询系统后端。用户填偏好,系统用LLM理解意图,动态生成下一步问题,最终引导找房流程。
核心复杂度在于:多步骤信息采集、LLM解析用户输入、动态问题生成。不是简单的增删改查,每一步都可能被LLM的随机性搞砸。
最终落地的七层结构
我最后定下的文件夹结构:
api/
core/
llm/
models/
repositories/
schemas/
utils/
看起来普通,但每一层的职责边界是踩过坑才划清的。
API层:越无聊越好
api/v1/endpoints/这层只干三件事:收请求、做校验、调下层。路由函数里不准出现业务逻辑,更不准直接调LLM。
比如这个 intake 接口:
@router.post("/intake")
def create_intake(...):
return intake_repo.create(...)
三行。所有脏活都扔给repository或LLM层。这层要是写得"有趣",三个月后就是灾难现场。
Core层:地基要稳
core/放配置、数据库连接、依赖注入、外部SDK封装。这些东西很少变,但一旦要改,影响面极大。单独一层是为了隔离变化。
比如数据库连接池、Redis客户端、第三方API的认证逻辑——全塞这里,上层通过依赖注入拿,不准自己实例化。
LLM层:把它当成一个独立业务域
这是整个结构最关键的设计。llm/下面按业务场景分子文件夹,比如 intake/ 里再分:
prompts.py —— 所有提示词集中管理
schema.py —— LLM输出必须匹配的结构
service.py —— 调用逻辑和错误处理
providers/ —— 不同供应商的适配
调用方式也很直接:llm_intake_service.parse_user_input(text)。其他层不知道背后用的是OpenAI还是Claude,也不知道提示词长什么样。
为什么要隔离?因为LLM有三重麻烦:输出不固定、对提示词极度敏感、随时可能换供应商。这三件事混在一起,不隔离的话,改一个提示词可能引发连锁崩溃。
Models与Repositories:数据访问要干净
models/只定义数据库表结构,repositories/只放查询逻辑。这个拆分来自Django和Rails的传统,但在FastAPI项目里经常被人忽略。
好处是测试时可以mock repository,不用碰真实数据库。LLM相关的数据操作也能单独验证,比如"用户输入→LLM解析→入库"这条链路。
Schemas:对抗幻觉的第一道防线
LLM会幻觉,会返回格式不一致的东西。schemas/这层就是用来堵窟窿的:定义严格的数据结构,每个LLM响应都必须过一遍校验。
流程变成:用户输入 → LLM提示 → LLM响应 → Schema校验 → 结构化数据。校验失败就重试或降级,绝不把脏数据往后传。
Utils:慎用,非常慎用
小工具函数可以放,但要有警觉。一旦utils/开始膨胀,就说明有些逻辑该下沉到具体业务层,或者该抽象成独立服务了。
我的标准是:如果某个函数被三个以上不同业务场景调用,才考虑放utils。否则就留在它该在的地方。
一个核心原则
把LLM当成独立业务域,而不是随手调用的工具。
这个认知转变影响一切:提示词需要版本管理,输出需要契约约束,调用需要熔断降级,成本需要监控告警。当你把它当作一个可能随时失控的外部系统来设计,结构自然就严谨了。
这结构不完美,但扛住了真实项目
三个月过去,prompts.py里的提示词改了十七版,providers/里从OpenAI切到Azure又切回来,intake流程加了两个新分支。但因为每层职责清晰,改动基本被锁死在局部。
结构不是为了好看。是为了当你凌晨两点排查一个LLM输出异常时,能快速定位问题在哪一层,而不是在十七个文件里跳来跳去。
最后说句实话:这个项目的utils/里现在有三个函数,我怀疑其中两个其实不该在那里。但至少我知道它们不该在那里——这比无知无觉地烂掉,已经强太多了。
热门跟贴