Vibe Coding全栈实战:功能跑通后架构收敛技巧精选
摘要
Vibe Coding 全栈实战:章鱼哥解题 07|功能跑通后的架构收敛 上一期实现了对话持久化,章
Vibe Coding 全栈实战:章鱼哥解题 07|功能跑通后的架构收敛
上一期实现了对话持久化,章鱼哥不再是那个只负责回复的接口。登录态、当前会话、LangGraph thread 均已就位,刷新页面后最近的对话消息也能完整恢复。

功能验证通过,但审视后端模块的依赖关系时,有两个隐患隐约让人不安。
第一个是 agent 依赖了 chat:
agent.nodes → chat.question_classifier
第二个是 infra.llm 依赖了 rag.context_builder:
infra.llm → rag.context_builder
这两处依赖当前不影响功能运行——测试通过、接口正常、页面可对话。但长期来看,它们会逐渐模糊模块边界:随着功能增量叠加,代码到底属于业务编排、基础设施还是 RAG 检索会变得越来越难以判断。
因此这一期没有新增功能,而是暂停下来做了一次小范围的架构收敛。
目标非常克制:
不改业务逻辑
不改函数签名
只移动模块归属
只更新 import 路径
最后用测试和残留引用检查验收
一、为什么功能跑通后还要重构
Vibe Coding 的核心优势在于快速验证。
从 RAG 检索、流式输出、鉴权接入到对话持久化,大量代码都是在“先打通链路”的节奏下完成的。这一阶段最关键的是验证方向:用户能否提问、后端能否回复、前端能否渲染、刷新后对话能否保留。
但功能一多,另一个问题就会浮出水面:AI 往往能写出局部正确的代码,却不擅长长期维护模块边界。
举个例子,问题分类最开始是 Chat 接口里的一段逻辑,放在 chat/question_classifier.py 很自然。后来引入 LangGraph 后,agent 也需要做意图分类,于是直接引用了 chat.question_classifier。
代码能跑,但依赖关系变成了这样:
agent → chat
这显然不合理。agent 和 chat 都是业务层模块,agent 不应该因为一个通用分类函数而去依赖 chat。
另一个类似的问题是 context_builder。
它的职责是把检索到的 chunk 组装成 LLM 可用的上下文——比如带编号的教材片段、引用来源列表。最初放在 rag 包里,因为输入来自 RAG 检索结果。但后来 infra.llm 和 agent.graph 都要用它,依赖变成了:
infra.llm → rag.context_builder
agent.graph → rag.context_builder
这里的根本问题不是函数写错了,而是位置放错了。context_builder 本质上属于“给 LLM 组装 prompt 上下文”的基础设施能力,不应该被归类为 RAG 检索功能。
因此这一期要做的事情很清晰:把模块迁移到更合适的位置。
二、先看重构前的问题
重构前的两个关键问题可以这样理解。
第一个问题是同层耦合。
agent.nodes → chat.question_classifier
agent 负责智能体流程编排,chat 负责对话业务接口和服务。分类器本身是一个无状态纯函数,只依赖正则规则,并不属于 Chat 接口的专有逻辑。它应该放在更底层、更通用的位置。
第二个问题是职责归属混淆。
infra.llm → rag.context_builder
infra.llm 是 LLM 调用实现,rag 的业务焦点应该是教材读取、分块、向量化、检索等。context_builder 负责把检索结果格式化为 LLM 可用的 prompt context,更适合与 LLM 基础设施放在一起。
这类问题短期内不会触发异常,但长期会持续恶化——后续如果添加新的智能体节点、评估逻辑、对话策略,大家都会习惯性地引用“恰好能用”的函数。最终代码仍然能跑,但模块边界会变成一张纠缠不清的网。
三、把 question_classifier 移到 domain
第一个调整是移动问题分类器。
重构前:
backend/app/chat/question_classifier.py
重构后:
backend/app/domain/classifier.py
为什么放到 domain?
因为它满足几个关键特征:
- 不依赖 FastAPI
- 不依赖数据库
- 不依赖 LLM
- 不依赖 ChatService
- 只根据问题文本判断
textbook/unrelated
也就是说,它是一个纯通用的领域判断函数。chat 可以用,agent 也可以用,后续如果评估或 API 层需要复用,也不应该反向依赖 chat。
调整后依赖关系变为:
agent.nodes → domain.classifier
chat.service → domain.classifier
代码层面没有改动任何分类规则,只更新了 import:
# 改前
from app.chat.question_classifier import classify_question# 改后
from app.domain.classifier import classify_question
这类重构最忌讳顺手修改逻辑。只移动、不改行为,才能确保测试失败时能迅速判断是迁移出错还是业务逻辑被破坏。
四、把 context_builder 移到 infra
第二个调整是移动上下文构建逻辑。
重构前:
backend/app/rag/context_builder.py
重构后:
backend/app/infra/context_builder.py
这里容易混淆的是:它处理的是 RAG 检索结果,为什么搬出 rag?
判断依据其实很简单:一个模块属于哪里,不仅要看它的输入,还要看它服务的对象是谁。
context_builder 的输入确实是 QueryResult,但它的输出是给 LLM prompt 使用的上下文文本和引用来源。它不是在执行检索,也不是在做分块,而是将检索结果转换成 LLM 可消费的格式。
因此它更接近 LLM 调用链路的一部分,而不是 RAG 检索逻辑的一部分。
调整后依赖变为:
infra.llm → infra.context_builder
agent.graph → infra.context_builder
对应的 import 同样只更换路径:
# 改前
from app.rag.context_builder import build_numbered_context
from app.rag.context_builder import chunks_to_sources# 改后
from app.infra.context_builder import build_numbered_context
from app.infra.context_builder import chunks_to_sources
函数签名不改、返回结构不改、调用方式不改。这样整个重构不会影响 RAG 检索质量,也不会影响 LLM 生成结果。
五、重构后的项目结构和依赖关系
这次虽然只动了两个后端模块,但依然把前后端放在一起重新审视了一遍。原因很简单:架构收敛不是只看某个文件放在哪里,而是要看它在整条产品链路中承担什么职责。
前端这一期没有做结构调整,它主要作为后端能力的调用边界放进架构图。重构真正发生在后端:classifier.py 从 chat 移到 domain,context_builder.py 从 rag 移到 infra。
重构后,项目结构可以简化成这样:
OctoTutor
├── frontend/src
│ ├── app # Next.js 路由入口
│ ├── chat # 对话状态、SSE、历史恢复等前端逻辑
│ ├── components # Chat UI、消息气泡、来源展示等组件
│ ├── contexts # 登录态、全局状态上下文
│ ├── hooks # 页面和组件复用的 React hooks
│ └── lib # API client、工具函数
└── backend/app
├── main.py # FastAPI 应用入口,装配依赖和路由
├── config.py # 环境配置
├── domain # 领域模型、协议、通用领域逻辑
│ ├── models.py
│ ├── protocols.py
│ └── classifier.py # 从 chat 移入:问题意图分类
├── rag # 教材读取、分块、向量化、向量存储
│ ├── models.py
│ ├── embeddings.py
│ ├── vector_store.py
│ ├── chunkers/
│ ├── readers/
│ └── classifiers/
├── infra # 外部能力和基础设施适配
│ ├── llm.py
│ ├── context_builder.py # 从 rag 移入:LLM 上下文格式化
│ ├── bm25.py
│ └── reranker.py
├── agent # LangGraph 编排和智能体节点
│ ├── graph.py
│ ├── nodes.py
│ └── prompts.py
├── chat # 对话 API 的业务编排
│ ├── service.py
│ ├── stream_router.py
│ ├── conversation_router.py
│ ├── schemas.py
│ └── dependencies.py
├── api # 其他 HTTP API 路由
├── ingestion # 教材入库流程
├── evaluation # 检索和回答质量评估
└── middleware # 鉴权等请求中间件
模块依赖关系可以简化成这样。虚线表示前后端之间的 HTTP 调用边界,实线表示后端内部的代码依赖。
这张图里最重要的变化有两处。
agent.nodes → domain.classifier
chat.service → domain.classifierinfra.llm → infra.context_builder
agent.graph → infra.context_builder
也就是说,agent 不再依赖 chat,infra.llm 也不再跨到 rag 包里取 prompt 组装逻辑。
需要说明一点:这次重构的目标不是让所有模块完全隔离。比如 infra.context_builder 仍然会使用 QueryResult 这类 RAG 数据模型。真正要收敛的是那些职责不清的依赖路径——LLM 调用不应该从 rag.context_builder 里拿 prompt 格式化逻辑,智能体节点也不应该从 chat 包里拿通用分类函数。
六、怎么验收这类重构
这类重构不需要复杂的验收流程,但验收不可省略。
因为表面上看只是“挪文件、改 import”,真正的风险在于:
- 旧路径有没有遗漏
- 调用方的行为有没有变化
- 模块依赖是否真的收敛
所以主要关注三件事。
第一件,确认旧引用已被彻底清理。比如不应该再出现:
from app.chat.question_classifier
from app.rag.context_builder
第二件,先跑与这两个模块直接相关的测试,再跑后端全量测试。分类器迁移后,问题分类结果要保持不变;上下文构建迁移后,LLM 生成链路和 StateGraph 仍然要能正常跑通。最后用全量测试确认本次移动没有影响到其他模块。
第三件,回到依赖图上确认目标是否达成:
agent 不再依赖 chat
infra 不再通过 context_builder 依赖 rag
domain 新增 classifier
infra 新增 context_builder
这里的关键不是把验收做得多么复杂,而是证明一个事实:这次只改变了模块归属,没有偷偷改变任何业务行为。范围越小,验收越清晰,这类架构收敛才不会变成另一场失控的大改。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。