← 返回博客
2026-09-11 08:00:01

LLM 应用架构:从单体到微服务的演进

LLM 应用架构:从单体到微服务的演进

一个客服问答系统,最初把 prompt 拼接、向量检索、模型调用全塞进一个 Spring Boot 服务里,二十个接口共用一份配置。上线三个月后,产品要换 embedding 模型、运营要调 prompt 模板、算法要试新的大模型,三拨人改同一份代码,谁合并谁冲突。这不是团队协作问题,是架构问题——LLM 应用的能力边界会持续变动,单体架构把变动成本放大了。

单体阶段:为什么一开始就该这么写

别急着上微服务。日调用量在十万次以下、模型和 prompt 一周改不了一次的场景,单体就是最优解。一个 ChatService 串起检索、拼接、调用,调试链路短,出问题一眼能看到底。

真正要解决的,是给未来的拆分留出缝。做法是在单体内部先把职责切开:

public interface LlmGateway {
    ChatResponse chat(ChatRequest request);
}

public interface Retriever {
    List<Document> search(String query, int topK);
}

@Service
public class QaService {
    private final Retriever retriever;
    private final LlmGateway llmGateway;
    private final PromptTemplate template;

    public QaService(Retriever retriever, LlmGateway llmGateway, PromptTemplate template) {
        this.retriever = retriever;
        this.llmGateway = llmGateway;
        this.template = template;
    }

    public String answer(String question) {
        List<Document> docs = retriever.search(question, 5);
        String prompt = template.render(Map.of("context", join(docs), "question", question));
        return llmGateway.chat(ChatRequest.of(prompt)).content();
    }
}

关键点在三个接口的边界:Retriever 只关心"给查询返回文档",LlmGateway 只关心"给请求返回补全",QaService 只做编排。踩坑最多的地方是把向量库的 SearchRequest 对象直接透传到 service 层,等于把 Milvus 的 SDK 焊死在业务代码里,将来换库要改遍所有调用点。接口参数用自己的 DTO,转换逻辑收在实现类内部。

这样写的单体,拆分时是把实现类换成 HTTP 客户端,不是重写业务逻辑。

拆分信号:什么时候该动手

三个信号出现任意一个,就该考虑拆了。

模型调用成为瓶颈。 单体里所有接口共享一个线程池,一个批量摘要任务把线程占满,问答接口跟着超时。模型推理是慢操作,秒级起步,它和普通 CRUD 抢资源必然出问题。

能力独立演进。 embedding 模型换一次,全量文档要重新向量化,这个过程想不影响在线问答。

成本要单独核算。 老板问"问答功能一个月烧多少钱",单体架构下 token 消耗混在总日志里,算不出来。

拆分的第一个动作不是拆服务,是把 LlmGateway 抽成独立进程。它承接所有模型调用,做三件事:统一鉴权、限流、token 计量。这一步做完,成本能算清了,模型切换也不影响上游。

微服务阶段:网关 + 编排 + 能力层

拆到位的结构是三层。网关层做路由和配额,编排层跑业务逻辑(原来的 QaService),能力层是检索、模型调用、工具执行这些可复用的原子服务。

能力层服务之间用 gRPC,编排层对上游暴露 REST。检索服务返回的文档要带来源标识,编排层拼 prompt 时把来源写进去——这是后面做引用追溯的前提。

一个容易忽略的点:编排层不要缓存模型响应。LLM 输出有随机性,缓存命中会让同一个问题返回不同答案,用户会以为系统抽风。要缓存就缓存检索结果,那部分才是确定性的。

拆完之后,原来单体里的 Retriever 变成远程调用,网络失败要处理。别在这里做复杂重试,检索失败降级成"无上下文直接问模型"就行,模型答不出来比整个请求挂掉好。重试逻辑放在网关层,统一处理。

踩坑总结

服务粒度太细。 有人把 prompt 渲染也拆成服务,每次问答多一次网络往返,延迟涨了却没有任何收益。判断标准很简单:这个服务会不会被两个以上上游复用?不会就别拆。

分布式追踪缺失。 一次问答跨四个服务,出问题不知道卡在哪。从第一天就接 OpenTelemetry,trace id 从网关透传到模型调用,日志里带上它。

配置散落。 prompt 模板、模型名、温度参数散在各个服务的配置文件里,改一次要发四个服务。收进配置中心,支持热更新。

版本兼容。 模型网关升级接口,编排层还在用旧协议。能力层接口加字段可以,改字段不行,用 protobuf 的字段编号管住这件事。

下一步

架构演进不是目标,是应对变化的手段。单体跑得动就别拆,拆之前先把接口边界切干净。真要动手,从抽出模型网关开始——它带来的成本可见性和模型可替换性,是后面所有拆分的基础。下一步可以把检索服务也独立出来,先量一下它和问答接口的 P99 延迟差多少,再决定值不值得拆。

本文关键词:LLM 应用架构、微服务、模型网关、向量检索、OpenTelemetry