27 存量系统接大模型:旁挂一个 AI 微服务,老架构一行不改

面试现场,面试官往椅背上一靠,抛出一个特别现实的问题:

"你们公司那套系统是老架构------MySQL 加 Redis,Spring Boot 跑了好几年,连 PostgreSQL 都没有。现在老板说必须接大模型。你怎么干?"

这个问题的坑,不在技术,在你上来就答"先上一套向量数据库"。面试官听完基本就凉了:老系统能让你随便动架构吗?运维同意吗?预算批得下来吗?上线窗口在哪?

存量系统接 AI,考的不是你会不会搭向量库,考的是你会不会按改造成本排优先级,知道每一步的天花板在哪。这篇文章给你一条最稳的路径:老架构一行不改,旁挂一个 AI 微服务,再配一个独立的向量库。


一、先别急着选型:你要的到底是哪件事

很多人一开口就聊向量数据库,其实压根没想清楚需求。老系统要接大模型,无非三种:

  • A. 只想要模型的"嘴" :文本生成、改写、总结、分类、抽取。数据从哪来?你自己用老办法查出来喂进去就行了。这种需求一个向量数据库都不需要。

  • B. 想要模型的"知识":让模型基于你的私有文档回答问题。这才是 RAG,才需要向量检索。

  • C. 想要模型的"手":让模型调用你的业务接口(查订单、改状态)。这是 Function Calling / Agent 的活。

关键结论:国内绝大多数老系统的第一个 AI 需求,是 A,不是 B。 客服话术、工单摘要、评论分类、报表解读------全是 A。上来就冲着 B 去搭向量库,是把简单问题做复杂了。所以正确的顺序是:先看需求属于哪一类,再决定要不要动数据层。


二、第 0 层:零新依赖,把模型当"上下文翻译器"

这一层的思路很简单:老系统负责"查事实",模型负责"说人话"。 数据还是从 MySQL 走原来的 SQL 查出来,拼进 Prompt,让模型基于这些事实组织语言。不建向量库,不改表结构,不动 Redis。

先看链路:

Java 侧用 Spring AI,代码短到你会怀疑人生:

java 复制代码
@Service
public class LegacyAssistant {

    private final ChatClient chatClient;
    private final JdbcTemplate jdbc;   // 老系统本来就有的东西

    public LegacyAssistant(ChatModel chatModel, JdbcTemplate jdbc) {
        this.chatClient = ChatClient.builder(chatModel).build();
        this.jdbc = jdbc;
    }

    public String askAboutOrder(String orderNo, String question) {
        // 1) 用老办法把事实查出来(还是原来那套 SQL)
        List<Map<String, Object>> rows = jdbc.queryForList(
                "SELECT order_no, status, amount, created_at " +
                "FROM t_order WHERE order_no = ?", orderNo);

        // 2) 拼成一段纯文本,顺手做脱敏和截断
        String facts = rows.stream()
                .map(r -> "单号:" + r.get("order_no")
                        + " 状态:" + r.get("status")
                        + " 金额:" + r.get("amount")
                        + " 下单:" + r.get("created_at"))
                .collect(Collectors.joining("\n"));

        // 3) 交给模型:只让它依据 facts 回答,不许自由发挥
        return chatClient.prompt()
                .system("你是客服助手。只能依据 <facts> 里的内容回答;"
                      + "信息不足就直接说不知道,禁止编造。")
                .user(u -> u.text("""
                        <facts>
                        {facts}
                        </facts>
                        用户问题:{q}
                        """).param("facts", facts).param("q", question))
                .call()
                .content();
    }
}

这段代码能跑,是因为 ChatClient、ChatModel 都是 Spring AI 现成的 Bean,JdbcTemplate 是你项目里本来就有的。整段代码零新组件。

这一层能扛什么、扛不住什么,心里要有一杆秤:

  • 能扛:低频的问答、摘要、分类、内容生成。数据量小、对延迟不敏感的场景,完全够用。

  • 扛不住:需要"从海量文档里找相关片段"的需求。你没法把十万篇文档塞进 Prompt,上下文窗口和 Token 成本卡得死死的。

  • 必须做的三件事:① 脱敏,别把手机号、身份证、金额明细直接喂给外部模型;② 截断,控制单次输入长度,超长就分段或只取摘要;③ 兜底,模型超时或报错要有降级路径,不能让它把主流程拖死。

一句话记住:第 0 层不是"凑合方案",它是成本最低、上线最快、风险最小的第一步。很多老系统的 AI 需求,到这一步就结束了。


三、主力方案:旁挂一个 AI 微服务,配一个独立向量库

需求升级到"要基于私有文档回答",就得做检索了。这时候最稳的做法,不是去改老系统,而是旁挂。

为什么是"旁挂微服务",而不是"改造老系统"

把 AI 能力塞进老系统里,听起来省事,其实是给自己挖坑:

  • 老系统一行不改:业务逻辑、表结构、部署流程全都不动,风险被隔离到最小。改老系统意味着回归测试、上线窗口、连带故障,全是成本。

  • 新服务独立扩缩容:AI 流量和业务流量互不干扰。大模型调用一慢,也只影响 AI 服务,拖不垮交易主链路。

  • 出问题能一键回滚:AI 服务整个下线,老系统的原有功能照样跑。

  • 技术栈解耦:老系统是 JDK 8 + Spring Boot 2 不敢动,新服务直接上 JDK 17 + 最新 Spring AI,各玩各的。

链路长什么样

  1. 老系统照常写 MySQL、跑 Redis,一个字都不改。

  2. 新增一个 ai-service(Spring Boot + Spring AI),它只干三件事:检索、拼 Prompt、调模型。

  3. 文档切片和向量,存到一个全新的 PostgreSQL + pgvector 库里------注意这是新库,不是老库,老库还是 MySQL。

  4. 需要实时数据时,ai-service 通过 HTTP 调老系统新暴露的查询接口,按需拉取,保证数据新鲜。

切片与入库:新库独立,不动老库

pgvector 只是 PostgreSQL 的一个扩展,建表很直接:

sql 复制代码
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE kb_chunk (
    id         BIGSERIAL PRIMARY KEY,
    doc_id     BIGINT      NOT NULL,
    tenant     VARCHAR(64) NOT NULL,   -- 用于权限过滤
    content    TEXT        NOT NULL,
    embedding  vector(1536)            -- 维度要和 embedding 模型对上
);

-- 建议先灌数据、再建索引,边建边灌很慢
CREATE INDEX idx_kb_chunk_vec
    ON kb_chunk USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

向量是应用侧算好再写进来的,pgvector 只负责存和查。这块的细节------HNSW 参数怎么调、三种距离算子怎么选、Spring AI 怎么接 pgvector------我在第 26 篇已经掰开揉碎讲透了,这里不重复,只说存量改造上真正独有的部分。

实时数据:老服务暴露接口,ai-service 按需拉

老系统只需增加一个只读查询接口,代码短得不能再短:

java 复制代码
@RestController
public class LegacyDataApi {                 // 挂在老系统里

    private final JdbcTemplate jdbc;         // 老系统原有组件

    @GetMapping("/api/tickets/latest")
    public List<Map<String, Object>> latest(@RequestParam String userId) {
        return jdbc.queryForList(
                "SELECT ticket_no, status, updated_at " +
                "FROM t_ticket WHERE user_id = ? " +
                "ORDER BY updated_at DESC LIMIT 5", userId);
    }
}

为什么是"按需拉",不是"同步进向量库"? 因为实时数据变得太快。工单状态一分钟变三回,你同步进向量库就开始滞后,用户问"我单子到哪了",模型拿着十分钟前的状态回答,比不回答还糟。实时数据永远走接口现查,向量库里只放那些变化慢的知识类文档。

ai-service 侧:检索 + 实时 + 拼 Prompt

java 复制代码
@Service
public class KnowledgeAssistant {

    private final VectorStore vectorStore;   // Spring AI + pgvector
    private final ChatClient chatClient;
    private final RestClient legacyClient;   // 调老系统的接口

    public KnowledgeAssistant(VectorStore vectorStore, ChatModel chatModel,
                              @Value("${legacy.api.base}") String legacyBase) {
        this.vectorStore = vectorStore;
        this.chatClient = ChatClient.builder(chatModel).build();
        this.legacyClient = RestClient.builder().baseUrl(legacyBase).build();
    }

    public String ask(String userId, String question) {
        // 1) 知识类内容:从独立向量库检索相关切片(带权限过滤)
        List<Document> docs = vectorStore.similaritySearch(
                SearchRequest.builder()
                        .query(question)
                        .topK(5)
                        .filterExpression("tenant == '" + userId + "'")
                        .build());

        // 2) 实时内容:调老系统接口现拉,保证不过期
        String live = legacyClient.get()
                .uri("/api/tickets/latest?userId={u}", userId)
                .retrieve()
                .body(String.class);

        String context = docs.stream()
                .map(Document::getText)
                .collect(Collectors.joining("\n---\n"));

        return chatClient.prompt()
                .system("只能依据 <docs> 和 <live> 的内容回答;"
                      + "没有依据就说不知道,禁止编造。")
                .user(u -> u.text("<docs>{d}</docs>\n<live>{l}</live>\n问题:{q}")
                        .param("d", context).param("l", live).param("q", question))
                .call()
                .content();
    }
}

三个要点,面试官很爱追:

  • 权限过滤走 filterExpression:在向量库这一步就把不属于该用户的切片挡掉,别等检索完再在内存里筛------既慢又容易漏。

  • 实时数据走接口现查,知识文档才进向量库:两条数据源,职责分清。

  • 接口要有超时和兜底:老系统的接口一旦抖动,ai-service 要能降级(用缓存、或只答知识部分),别把整个问答拖死。


四、Redis 别当向量主库,但语义缓存是真香

铺垫了这么久,回过头回答一个高频追问:老系统本来就有 Redis,为什么不直接拿 Redis 当向量库?

因为对"存量系统"这个前提来说,它不够站得住:

  • 版本门槛 :老的开源 Redis 不带模块,压根没有向量能力。要用得换 Redis Stack ,或者升级到 Redis 8+ 的原生 vector sets。你手上那台跑了好几年的 Redis 大概率不是------那这就不叫"不加组件"了。

  • 纯内存,成本随规模陡增:按 float32 算,1536 维一条向量约 6KB,100 万条光原始向量就 6GB,再加 HNSW 图结构的开销,一台十几 G 内存的机器直接吃满。几十万条以内很香;上千万条,就该上独立向量库了。

  • 高可用和集群要自己扛:主从、持久化、集群分片下的索引一致性,用 Redis 做向量比用专用库要多操不少心。

所以对存量系统,把向量检索交给独立的 pgvector 库,比硬榨 Redis 更干净。

不过 Redis 有一个技能对老系统是性价比之王------语义缓存。用户问"退货要几天"和"几天内能退",传统缓存当成两个 key,各调一次模型;语义缓存用向量比对,判定是一回事,直接返回缓存结果。不上任何新组件,就能砍掉一大截模型调用成本。

java 复制代码
SemanticCacheAdvisor cacheAdvisor = SemanticCacheAdvisor.builder()
        .cache(semanticCache)   // 底层还是 Redis 向量检索
        .build();

String answer = ChatClient.builder(chatModel)
        .build()
        .prompt()
        .user(question)
        .advisors(cacheAdvisor)     // 命中就走缓存,绕过模型
        .call()
        .content();

一句话:Redis 拿来省模型调用,不拿它扛向量主库。


五、数据量和并发上来之后:CDC 旁路增量 + 回填

主方案跑起来后,会冒出一个新问题:文档更新了,向量库怎么跟着更新?

如果文档变动不频繁,最简单是定时任务重新灌 。但数据一多、要求一变高,就得上真正的增量链路:CDC(Change Data Capture)旁路同步------主库一行不改,从 binlog 里"偷"变更。

链路是这样的:

  1. 业务照常写库,一个字都不用改。

  2. 用 CDC 工具订阅 binlog------国内常用 Canal ,也有人用 Debezium。

  3. 消费变更消息,把新增/修改的文档送去重新切片、算 embedding。

  4. 写入独立向量库,旧系统这边只多一个 HTTP 客户端。

这条链路能把风险隔离得最干净:向量库挂了不影响业务,重建索引不用停机,模型换了一边重灌一遍就行。

但旁路同步有四个必须处理的现实问题,面试官很爱追:

  • 同步延迟:binlog 消费是有延迟的。刚改完的数据马上问,模型可能还拿着旧答案。要么接受这个窗口,要么在关键路径上做"读己之写"的兜底。

  • 回填(backfill):系统上线那天,存量数据怎么一次性灌进去?得有一套能断点续跑的初始化脚本。

  • 幂等:binlog 可能重放,同一条记录被消费两次不能插出两份向量。用业务主键做去重。

  • 删除:上游删了数据,向量库里的也得跟着删,否则模型会拿"已死的文档"回答问题。这块最容易被漏掉。


六、真正决定能不能上生产的,是这几件"横切事"

方案搭起来只是及格,能不能上线,看的是下面这些:

  • 降级 :大模型接口挂了怎么办?必须有兜底------退回关键词检索、退回规则话术、退回上一条缓存。让 AI 功能的失败不要变成业务功能的失败。

  • 灰度开关:新功能一定要有开关,能按用户、按流量比例放量,出问题一键关掉。

  • 成本与限流:给模型调用加限流和配额,别让一次异常流量把账单刷爆。语义缓存也是在这里发力的。

  • 权限与脱敏:喂给模型的数据要按权限过滤,敏感字段要脱敏。老系统的权限模型本来就在,接 AI 时别绕过去。

  • 可观测:把每次调用的耗时、Token 数、命中缓存的比率都打点,不然出了问题两眼一抹黑。

这五条听着像"运维八股",但它们才是第 0 层方案和生产级方案的真正分界线。第二章那段代码能不能变成线上功能,差的就是这些。


七、落地顺序:从第 0 层走到 CDC

把上面的东西串成一条可执行的路径:

  1. 先做第 0 层,挑一个高频、低风险、错了也不出事的场景(比如客服话术、工单摘要)。上线,收集真实使用数据。这一步验证的是"这个需求到底有没有用"。

  2. 需求升级到文档检索,再旁挂 ai-service + 独立 pgvector 库,实时数据走老系统接口现查。这一步验证的是"检索质量够不够用"。

  3. 数据量或更新频率压不住,才上 CDC 旁路增量(加存量回填)。这一步验证的是"成本和收益划不划算"。

  4. 全程把第六节那五件横切事补齐,缺一件都别急着放量。

每一步都有明确的"触发条件",没到就不往下走。这就是"按改造成本排优先级"的具体含义。


🎯 面试官视角的标准回答

被问"存量老系统怎么接大模型",可以这么答:

"我不会一上来就动架构。先看需求属于哪一类------只是要模型的表达能力,还是要基于私有知识检索,还是要让模型调业务接口。多数老系统的第一个需求其实只是第一类,用现有的 SQL 查数据、拼进 Prompt 就够了,一个组件都不用加。<br><br>需求升级到文档检索,我的做法是旁挂:新增一个 AI 微服务跑 Spring AI,切片存到独立的 pgvector 库,老系统一行不改;实时数据让老系统暴露只读接口,ai-service 按需现拉,不进向量库------因为实时数据进库就滞后了。<br><br>有人会说用现成的 Redis 做向量库,但存量系统那台 Redis 往往不带向量能力,要换 Stack 或升 8+,等于还是改架构;而且纯内存,规模一上来成本陡增。所以向量检索我交给独立库,Redis 只留着做语义缓存省成本。<br><br>等更新频率上来,再上 CDC 旁路同步做增量。整个过程里,我最在意的是降级、灰度、成本、权限和数据一致性这几件横切的事------方案能不能上生产,差别就在这些地方,不在选哪个向量库。"

这套回答之所以能打,是因为它同时展示了三样东西:需求判断力、成本意识、和生产风险意识。面试官要的就是这个。


到这儿,「面 Java 岗,问我 AIGC?」这个系列就 27 篇了。从 AIGC 概念一路讲到存量系统落地,主线始终是一句话:技术选型没有标准答案,只有"看你手里有什么牌、要解决什么问题"。 后面如果还有值得挖的场景,我们接着聊。

相关推荐
kaixin_啊啊1 小时前
【用于更准确 G×E 基因组预测的大规模作物数据集与基于深度学习的多模态融合框架】论文学习
人工智能·深度学习·学习
GOWIN革文品牌咨询1 小时前
工业测量品牌系统化搭建指南:从内容架构到长期治理的全流程
大数据·人工智能·架构
拿本唠嗑AI研究2 小时前
本体工程实战指南:从建模到可维护的完整落地路径
java·人工智能·python·jena·ontology
deepseek232 小时前
AI 模型隐藏任意能力拆解:把加密神经网络塞进宿主权重,能力评估为什么在密码学上抓不到
人工智能·深度学习·神经网络·密码学·模型安全
揽秀亭长2 小时前
从音频到五线谱,自动扒谱的完整技术流程
人工智能
Smoothcloud润云2 小时前
PyTorch推理服务模型热切换与失败回滚实战:降低版本更新中断
服务器·人工智能·pytorch·python·深度学习·gpu
冬奇Lab2 小时前
开源项目第236期:PentAGI — 全自主渗透测试多智能体系统,自带 20+ 专业工具箱
人工智能·开源·资讯
forest_092 小时前
Agent 系列02|Agent 架构全景图
人工智能·架构·prompt·agi
YangYang9YangYan2 小时前
2027供应链计划校招:数学与应用数学专业如何把预测模型讲成业务决策?
人工智能·sql·算法