三年前我会直接写路由代码。现在先问:这个项目3个月后怎么崩?
特别是接了大模型(LLM)之后,崩的方式更花哨。这篇分享一个真实房产咨询系统的后端架构——不是Demo,是扛过需求变更的代码结构。
从Express惯性到FastAPI的陷阱
Node.js老玩家熟悉这套:routes/、controllers/、services/、models/。灵活,简单,烂起来也快。
FastAPI给的是另一套问题:工具很强,结构零意见。结果就是代码长成这样——
api/、core/、llm/、models/、repositories/、schemas/、utils/
一旦LLM介入,混乱指数翻倍。用户输入→模型解析→结构化输出,每一步都可能出戏。
这个项目的需求很典型:收集用户偏好,用大模型理解意图,指导房产搜索。不是纯CRUD,有状态、有推理、有外部依赖。
第一层:API层,故意写得很无聊
路径:api/v1/endpoints/
代码长这样:
@router.post("/intake")
def create_intake(...):
return intake_repo.create(...)
没有业务逻辑,没有模型调用,纯路由。作者的原话:「Keep It Boring」。
这层只干一件事——接收请求、校验参数、转发给下层。好处是后期换LLM提供商、改解析策略,路由文件不用动。
很多项目崩在这:路由里塞prompt工程,结果一个接口改五遍,上下游全乱。
第二层:Core,整个系统的地基
core/目录下放全局配置、数据库连接、异常处理。所有跨模块的依赖都在这里初始化,不让具体业务代码操心。
关键设计:LLM不是工具函数,是独立领域。
core/llm/的结构:
intake/
prompts.py
schema.py
service.py
providers/
prompts.py管提示词模板,schema.py管输出结构,service.py管调用逻辑。providers/封装不同厂商的API差异。
调用方式:llm_intake_service.parse_user_input(text)
为什么拆这么细?prompt版本迭代比业务代码快十倍。把提示词硬编码在路由里,两周后就成屎山。
第三层:数据层,模型与仓库分离
models/ → 数据库表结构定义
repositories/ → 查询逻辑封装
作者从Express带来的教训:ORM模型直接塞查询,测试时想mock数据库,发现耦合死在代码里。
现在repository层是纯粹的数据访问接口,单元测试可以整体替换掉。LLM输出要入库?走repository,不走模型裸调。
第四层:Schemas,LLM的救命稻草
schemas/目录原本是给Pydantic校验用的。但LLM场景下,它成了关键基础设施。
流程变成:
User Input → LLM Prompt → LLM Response → Schema Validation → Structured Data
大模型输出是概率性的,今天能解析的JSON明天可能缺个字段。Schema校验是最后一道防线,崩了能catch住、能重试、能打日志。
作者没明说但藏在这的设计:schema同时服务两个主人——API接口契约和LLM输出约束。一份代码,两处收益。
第五层:Utils,别急着往里塞
utils/目录存在,但作者警告:好地方,也是垃圾堆。
适合放的:通用文本处理、日期格式化、重试装饰器。不适合:业务逻辑、临时补丁、「以后整理」的代码。
一个判断标准:如果某个函数只在LLM模块用,就留在llm/里。utils/的东西必须跨模块通用,否则就是假复用。
架构的隐藏收益
这套结构跑3个月后,作者发现个意外好处:换LLM供应商只改providers/目录,业务代码零感知。从OpenAI切到Claude,半天搞定。
prompt迭代更夸张:A/B测试直接改prompts.py,回滚秒级。如果提示词焊死在路由里,这种灵活性想都别想。
最后一句实话:没有架构能阻止项目崩,但好的结构能让崩的时候知道该修哪。
热门跟贴