三年前我会直接写路由代码。现在先问:这个项目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,回滚秒级。如果提示词焊死在路由里,这种灵活性想都别想。

最后一句实话:没有架构能阻止项目崩,但好的结构能让崩的时候知道该修哪。