news 2026/10/18 9:58:23

Spring AI + RAG旅游问答知识库:本地部署与阿里云百炼双方案实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI + RAG旅游问答知识库:本地部署与阿里云百炼双方案实战

最近在给一个旅游项目做问答机器人,需求很直白:游客问“XX景区几点开门”“周边有什么好吃的”“带老人小孩怎么玩”,系统得给出靠谱答案,而不是让大模型胡编。试过直接用通用大模型,效果确实“太通用”——能聊,但不知道景区的真实规则和实时信息。于是转了一圈,还是回到RAG(检索增强生成)这条路上,用Spring AI把检索和大模型串起来,既能在本地跑通一套完全私有化的方案,也能切到云上的托管服务。这篇文章就把我整个从入门到落地的过程完整拆开,包含两套方案:本地Embedding+向量库+本地LLM,以及阿里云百炼的托管链路,代码和踩坑记录都在里面,适合正在选型或准备动手写RAG的同学直接抄作业。

1. 为什么做旅游问答知识库,而不是直接让大模型回答

1.1 旅游咨询的真实痛点

旅游问答和一般闲聊不一样,对时效性和准确性的要求非常变态。比如景区门票价格会调整、淡旺季开放时间不同、某个景点临时关闭检修,这些信息大模型训练时根本没见过,强行回答就是一本正经地编。我这边拿到的资料是一堆文档:景区介绍PDF、游客常见问题Word、当地美食列表、交通指引说明,零散得很,好几个是业务人员手动整理的表格。

用户真正想要的是“从这些资料里找出答案,然后用自然语言回复”。如果拿这些文档直接丢给大模型做长上下文,不是不行,但有几件事会被卡住:文档超过上下文窗口直接被截断;多份文档混在一起,大模型分不清哪句话是哪个景区的;每次对话都重复传一大坨文本,成本高、延迟不可控。所以需要一个机制,先把资料“存”起来,用户提问时只把相关资料“捞出来”喂给大模型,这就是RAG。

1.2 RAG解决什么问题

RAG的原理说穿了很简单:把知识库文档拆成小块,做向量化存到向量数据库;用户提问时,把问题也向量化,去向量库里做相似度检索,找出最相关的几个文本片段;最后把“用户问题+检索到的片段”一起塞给大模型,让它基于这些片段生成回答。

这带来几个直接收益:一是答案有出处,模型只能基于检索到的内容回答,我还能把参考来源一并返回;二是知识更新快,资料变了只需要重新拆和存,不用重新训练模型;三是可控,哪些文档能用于回答完全由检索范围决定,不会让模型乱扯别的。实际体验下来,只要检索质量到位,正确率比裸调大模型高一大截,尤其是“某景点几点开门”这类具体问题。

1.3 双方案选型:本地 vs 云上

我做这套系统的时候,面临一个现实选择:模型和向量库到底放哪里。如果只求快速上线,直接用云厂商的托管服务肯定最省心,但如果是内部项目或对数据敏感,谁也不想把景区经营数据传到外部。于是我用Spring AI的抽象接口,把方案做成可切换的:本地一套,云上一套。

本地方案的链路是:本地Embedding模型做向量化 → Chroma或PGVector存向量 → Ollama跑本地大模型生成回答。全程数据不出内网,断网也能用,适合要求私有化部署的场景。云上方案用的是阿里云百炼平台,它把向量模型、大模型、知识库管理都做成API,Spring AI通过一个starter就能对接,适合快速迭代、不想折腾GPU的环境。两个方案背后的业务逻辑、Prompt模板、文档拆分策略是完全一致的,只是依赖配置和几个Bean的实现不同,这正是Spring AI框架最舒服的地方。

2. Spring AI 项目搭建与核心概念

2.1 工程初始化

我用的Spring Boot 3.2 + Spring AI 0.8.1,Java 17。Spring AI目前还在快速迭代,版本号变化大,建议以当前Maven中央仓库发布的最新RELEASE为准。建工程时直接加上核心依赖:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>0.8.1</version> </dependency>

如果需要对接百炼,还需要引入对应的Spring Cloud Alibaba AI模块(或者手动走HTTP API)。我图省事,用了官方提供的Spring AI Alibaba扩展,它的坐标是spring-ai-alibaba相关starter,版本要跟spring-ai-core匹配,否则启动时会遇到ClassNotFound。

实际工程里,我还加了这些库:spring-boot-starter-web、pgvector(本地向量库)、pdfbox/tika(解析PDF和Word)、commons-io。如果只用百炼方案,本地向量数据库那套可以完全不要,直接让百炼的知识库帮你管理文档,后面我会对比两种场景。

2.2 Spring AI 的抽象接口:EmbeddingModel、VectorStore、ChatModel

Spring AI对LLM生态做了很好的抽象,核心就三个接口,理解这三个就理解了整个框架:

EmbeddingModel负责把文本转成向量数组,本地方案用OllamaEmbeddingModel或ONNXEmbeddingModel,云上方案用百炼的实现;VectorStore负责向量的增删查,本地可以用PgVectorStore或ChromaVectorStore;ChatModel负责对话生成,本地用OllamaChatModel,云上用百炼的DashScopeChatModel。

因为接口统一,业务上层代码根本不关心底层是什么实现。我写了一个KnowledgeService,里面注入VectorStore和ChatModel,通过配置切换Profile,就能在本地和百炼两套环境之间无缝横跳,这个设计省了后面大量重复代码。

2.3 旅游知识数据准备

数据是RAG的地基。我这边整理出一批示例数据,结构大致如下:

  • 景区基础信息:名称、地址、门票价格、开放时间、联系电话
  • 交通指南:公交线路、停车场情况、从市区过去的大致时间
  • 游玩建议:推荐路线、游玩时长、适合人群
  • 常见问题:能不能带宠物、有没有行李寄存、军人是否免票

为了演示,我把这些信息切成一个个JSON或Markdown片段,每条包含content和metadata(比如景区名、文件来源)。数据准备这一步千万别偷懒,宁可多写一些专属描述,也不要只给干巴巴的结构化字段。比如“开放时间:08:00-17:00”不如“XX景区全年开放时间为早上8点到下午5点,冬季12月到2月调整为8点半到下午4点半”,因为向量模型对完整句子编码更友好,检索效果会好很多。

3. 本地方案实操:向量化、存储、检索与生成

3.1 本地嵌入模型选用与加载

本地方案第一个坑就是嵌入模型选择。旅游问答有大量中文景区名、地名,通用英文向量模型效果很差。我最终选了BGE的中文嵌入模型,用ONNX格式在本机跑。Spring AI对ONNX的支持还不错,配置如下:

@Bean public EmbeddingModel embeddingModel() { OnnxEmbeddingModel model = new OnnxEmbeddingModel( "src/main/resources/models/bge-small-zh-v1.5.onnx", "src/main/resources/models/tokenizer.json" ); return model; }

这个模型体积只有100多MB,CPU就能跑,对几十万文档的规模完全够用。如果你不想自己下模型,也可以用Ollama拉取nomic-embed-text,再通过OllamaEmbeddingModel调用,但注意Ollama的Embedding模型对中文支持参差不齐,建议实测后再定。本地有GPU的话可以换bge-large-zh,效果更好,但内存占用和推理时间都会上去。

做向量化时有一个细节:维度。BGE小模型的维度是512,PGVector建表时必须指定维度,而且要跟模型维度一致,否则存数据直接报错。我建表用的语句是CREATE TABLE IF NOT EXISTSvector_store(id UUID PRIMARY KEY, content TEXT, metadata JSON, embedding vector(512))。

3.2 向量库选型:Chroma还是PGVector

本地方案我建议优先PGVector,原因很简单:旅游业务方可能还要顺便存景区订单、用户反馈,用PostgreSQL统一存储,运维成本最低。Spring AI有专门的PgVectorStore,依赖一个DataSource就能初始化。

Chroma也不错,轻量、无需外部数据库,适合纯Demo。但一旦数据量大起来,Chroma的批量检索性能和稳定性不如PGVector。我这次先在Chroma上快速验证思路,后面正式环境切到了PGVector,切换代价很小,因为VectorStore接口是一样的。

3.3 文档分块与向量存储实现

RAG检索质量一半取决于分块策略。我一开始图省事,按固定字符数切,结果一句话被切成两半,检索出来语义不完整,大模型当然答不准。后来改成按段落和语义边界切:

  • 优先按段落拆,段落太长再按句子拆;
  • 每个分块控制在200~500字之间;
  • 同一个景区的多个片段,在metadata里打上统一的景区ID;
  • 对表格类内容,先转成描述性文本再切。

核心实现大概长这样:

public List<Document> splitDocuments(List<Document> docs) { List<Document> result = new ArrayList<>(); TextSplitter splitter = new TokenTextSplitter(300, 100); for (Document doc : docs) { result.addAll(splitter.apply(doc)); } return result; }

写入向量库时用vectorStore.add(docs),Spring AI会自动调用EmbeddingModel做向量化然后入库。这里建议先验证一下写入数量和id是否一致,我遇到过数据重复插入的情况,因为没做幂等控制。实际落地应该用docId或者文件内容的MD5去重。

3.4 相似度检索与问答接口实现

当用户问“故宫几点开门”,系统会先把问题向量化,然后查向量库,取top-k个相关片段,再拼Prompt发送给本地大模型。Spring AI提供了QuestionAnswerAdvisor,可以直接在查询里指定“基于以下上下文回答”,也可以自己拼Prompt拿更多控制权。我是自己实现的,因为要处理返回来源:

public String ask(String question) { List<Document> docs = vectorStore.similaritySearch( SearchRequest.builder().query(question).topK(5).build() ); String context = docs.stream().map(Document::getContent) .collect(Collectors.joining("\n---\n")); String prompt = """ 你是一个旅游助手。请只根据下面的资料回答问题。 如果资料里没有,就回答“抱歉,资料库中没有相关信息”。 资料: %s 问题:%s """.formatted(context, question); return chatModel.call(prompt); }

这里topK=5是我试出来的平衡值。K太小容易漏,K太大会把不相关的内容混进来,导致模型“被带偏”。另外检索本身还可以做阈值过滤,比如相似度低于0.6的直接不要。Spring AI的SearchRequest可以通过设置similarityThreshold来做,不过不同向量模型分数范围不一样,建议先用一批真实QA测出合适阈值。

本地大模型我用Ollama跑qwen2.5:7b-instruct,这个模型中文问答能力够用,7B的参数量在16G内存的机器上也能流畅跑。调用配置:

@Bean public ChatModel chatModel() { return OllamaChatModel.builder() .ollamaOptions(OllamaOptions.builder() .model("qwen2.5:7b-instruct") .temperature(0.3) .build()) .build(); }

温度调低是为了让回答更稳定,不做发散创作,毕竟是知识库问答而不是闲聊。实测在求解“XX景区门票价格”这类事实型问题时,温度0.1~0.3比较合适。

4. 阿里云百炼方案实操:托管模型 + 托管知识库

4.1 百炼平台的基本能力与产品逻辑

如果不想本地维护模型和向量库,百炼是一条更轻的路。它把大模型、Embedding、知识库、应用编排都做成了平台能力,你甚至可以不用写代码,先在控制台把知识库传上去、建一个应用、测试满意后再把API接入Spring AI。

百炼的逻辑可以简单理解为三个部件:数据中心(管理知识文档)、模型中心(各类大模型与向量模型)、应用中心(把知识库和大模型编排成一个问答应用)。Spring AI对接时,最核心的是通过DashScopeApi调用百炼的模型API,配置好API-KEY就能用。

4.2 创建应用与获取API Key

在百炼控制台先开通模型服务,然后创建API-KEY。实际项目里API-KEY不要硬编码在代码里,用环境变量或配置中心管理。之后把旅游相关的PDF、Word、Excel上传到“数据中心”的知识库,平台会自动做拆分和向量化,这一步比自己本地做要省心很多,还支持自动更新。如果你数据量不大,直接用控制台上传就行;如果数据有定时更新要求,可以调百炼的“文档导入”OpenAPI,把更新流程自动化。

创建完知识库后,还需要在应用中心创建一个应用,关联这个知识库并选择模型(比如qwen-plus)。控制台会让你设置“系统Prompt”,我的系统Prompt是:“你是某省旅游助手,回答时只使用客户提供的知识库内容,不得编造。如无法回答请明确告知。” 这样调好之后,控制台直接能测试问答效果,效果满意再接入代码。

4.3 Spring AI 对接百炼的配置

在工程里引入Spring AI Alibaba相关依赖后,配置非常简单:

spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus embedding: options: model: text-embedding-v3

然后代码里注入ChatModel和EmbeddingModel,跟你本地方案用的接口完全一样。这意味着如果之前本地方案已经写好了KnowledgeService,切换到百炼几乎不用改业务代码,只需要换配置和依赖。

有一个细节要注意:百炼的向量模型是text-embedding-v3,它输出的向量维度可能和本地BGE不同,所以如果你在本地已经有一个向量库,想迁移到百炼云上,需要重新对全部文档做一次向量化,不能直接沿用旧的向量数据,否则维度不匹配,检索结果也会乱套。

4.4 百炼知识库的API集成方式

大多数情况下,接入百炼有两种签名思路:

第一种,走平台已有的“应用调用”接口。百炼为控制台创建的应用生成一个专属的app_id,调用时只需要传用户问题,平台会把知识库检索和大模型生成都消化掉,返回最终答案。这种集成最省事,代码量最少。Spring AI里可以用DashScopeApi的chatCompletion直接调,也可以自己封装一个HTTP客户端去调百炼的应用Api。我这边为了跟本地方案共用一套KnowledgeService,最终还是把百炼当做一个标准的ChatModel来用,知识库检索则让百炼平台负责,代码里只负责组织输入输出。

第二种,把百炼当作纯模型API,自己来检索知识库。这个方式适合你已经有自己的向量库、不想把数据传到平台的情况。用百炼的EmbeddingModel做向量化,用你自己的VectorStore存储和检索,最后调用百炼的ChatModel生成。相当于混合架构:数据自己存,模型用云上。这是很多企业实际落地的折中方案,数据不一定要全交出去。

我用的是第一种,因为对旅游这种非敏感业务,托管知识库真的很爽。上传文档后,平台帮我做了拆分和索引,我甚至不需要关心分块粒度。但你得接受一个风险:知识库内容会传到云厂商那边,敏感数据慎用。

5. 双方案对比与选型建议

5.1 本地方案和云上方案的实测对比

我先后把同样的数据跑在两套方案上,用20条旅游FAQ做测试,结果如下:

维度本地方案百炼云上方案
部署环境一台16G内存的服务器即可无需GPU,仅需网络
数据隐私数据完全内网闭环知识库数据上传至云端
中文效果依赖选的中文模型,BGE+Qwen效果不错默认效果好,尤其长文档理解
首token延迟本地7B生成较慢,约2-5秒托管模型通常1-3秒
运维成本需要维护Embedding、向量库、LLM三个组件平台统一运维,故障率低
扩展性扩容需要自己加机器平台侧扩容,简单
离线可用支持断网运行不支持离线
开发复杂度中等,需要处理模型加载和向量库低,配置+API调用即可

在回答“哪些景区有停车场”这类需跨多个文档归纳的问题时,百炼的效果明显更稳定,因为qwen-plus的长文本理解能力比本地7B模型强不少。而回答“某景区开放时间”这种单点事实问题,两者不相上下。

5.2 不同场景下的方案选择

如果你是做一个内部演示系统,或者数据比较敏感(比如政务、医疗、企业内部FAQ),强烈建议本地方案。成本其实不高,BGE小模型免费,Qwen等开源模型免费商用,一台普通服务器就够了。最大的开销是运维时间,需要自己处理模型依赖和向量库备份。

如果你是做面向C端的旅游小程序,或者业务更新频繁、团队没有专业的模型运维人员,直接上云上方案,把精力放在知识库内容整理和Prompt调优上。云上还能方便地切换更大更强的模型,比如业务高峰期换qwen-max,平时用qwen-plus平衡成本。

5.3 混合思路:本地向量检索 + 云端生成

我实际落地到最后,选择了混合路线:用百炼的文本向量模型做离线向量化(好处是不占用本地资源),向量存在本地PGVector里,检索也在本地,最后调百炼的大模型生成答案。这样既避免了把最终知识库整个上云,又享受了云端大模型的效果。

实现上也不复杂,就是把EmbeddingModel换成百炼的,VectorStore继续用PGVector,ChatModel换百炼的。唯一要注意的是,百炼Embedding API会对单次请求文本长度有限制,批量向量化时要做分批处理。另外,云上模型API偶尔会有网络波动,本地至少要做一个超时重试和降级策略。

6. 常见问题与排查技巧实录

6.1 中文检索效果差,查出来的片段不相关

这种情况八成是Embedding模型不支持中文,或者文档分块不当。我一开始用某个英文模型,查询“南山公园怎么走”跟“南山公园”相关的文档得分反而不高,后来换成BGE中文模型,效果立竿见影。分块上,如果是把一篇景区介绍粗暴按200字硬切,经常出现“停车场在…”、“门票价格为…”这种半句话,检索出来当然不完整。建议分块时以句号为边界,同时保留段落上下文。

另外可以做一个评测集:整理50~100个真实用户问题,标注每个问题期望命中的文档片段,然后用这些评测去调分块粒度、topK、相似度阈值。没有评测集你永远不知道改动是好是坏,这是我实际踩坑后才补上的。

6.2 大模型“强行回答”资料里没有的内容

这在RAG里非常常见,尤其本地小模型更容易犯。解决办法有两个:一是在Prompt里强调“如果资料中没有明确提及,必须回答不知道”;二是对温度参数做限制,不要给模型自由发挥的空间。如果还是不听话,可以在代码里加一个校验:检查生成的答案里是否包含检索片段里的关键实体,不含的话就回复“未找到相关信息”。这种方法虽然粗暴,但在严肃业务里真的很实用。

6.3 API调用超时和限流

百炼API有QPS限制,Spring AI默认的RestClient超时时间可能只有5秒,长文档或者高峰期容易超时。我在配置里把连接超时和读取超时都调到了30秒,同时加了重试机制,针对429限流错误做指数退避重试。对于批量向量化请求,建议每批次不要超过50条文本,否则单条文本太长也容易超时。

6.4 本地模型Ollama偶尔变慢

Ollama在处理并发请求时会排队,旅游问答虽然并发不高,但一旦有人问了长问题,后面的请求会卡住。我这边把OllamaChatModel的并发参数调大,并给模型配置了OLLAMA_NUM_PARALLEL=4。如果机器内存足够,也可以换更大的模型,效果和速度都会更好。还有一点:不要让Ollama和PGVector跑在同一台2C4G的小机器上,否则内存紧张会导致系统OOM,建议至少8G内存起步。

6.5 安全合规与API Key管理

这个是很多人容易忽略的。代码仓库里一旦提交了API-KEY,基本等同于泄露。我在项目里用了Spring Boot的@ConfigurationProperties配合环境变量,本地开发用.env,生产环境用密钥管理服务。另外,面向用户开放的问答接口一定要接入频率限制,防止有人用你的接口免费刷大模型,导致账单爆炸。旅游问答还好,一旦涉及敏感信息,还要在检索结果后做一轮内容过滤,把不适宜展示的内容拦掉。

7. 写在最后的一些体会

这一套从零到上线做下来,我最大的感受是:RAG的技术栈并没有多神秘,真正难的是把文档整理得适合检索,以及把检索结果和Prompt设计打磨到和业务匹配。Spring AI帮我把大部分样板代码省掉了,让我可以在一套代码里随意切换本地和云端模型,这个抽象价值非常大。

最后分享一个小技巧:在similaritySearch返回结果时,不要把原始片段直接拼给大模型,最好在片段前面加上“来源:某文档某段落”,让大模型在回答时把引用一并带上。我后来加了这么一段,业务方看到答案时能直接跳转到原文出处,整个系统的可信度一下子就不一样了。如果你的旅游知识库后续也要做多客服、多语种支持,RAG这条路完全可以继续延伸下去,核心链路是不用动的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 9:55:20

基于SpringBoot+Vue的课外活动管理系统设计与实现

1. 项目整体思路与技术选型前后端分离已经是现在做课外活动管理系统的主流姿势&#xff0c;但真正从零开始把分支打全、逻辑理清楚&#xff0c;还是要费不少功夫。这套基于JavaSpringBootVue的课外活动管理系统&#xff0c;核心解决的是高校/中职院校里“活动发布—报名—签到—…

作者头像 李华
网站建设 2026/10/18 9:54:29

降AI指令实战:大模型调优让论文从机器味变成真人味

1. 为什么论文一出来就一股AI味你要的“大模型调优”&#xff0c;并不是把提示词写得多华丽&#xff0c;而是先理解一件事&#xff1a;机器味不是语法错误&#xff0c;是一种“平均脸”。单看每一句都没毛病&#xff0c;甚至找不到一个错别字&#xff0c;但连成段之后&#xff…

作者头像 李华
网站建设 2026/10/18 9:53:10

Java工程师的Agent工程化实战路径:24周从Spring到智能体系统

1. 这不是转行&#xff0c;是能力栈的结构性升级&#xff1a;一个Java老手的真实Agent工程化路径“Java工程师转型Agent Engineer”这个标题最近在技术社区刷屏&#xff0c;但很多人第一反应是&#xff1a;又一个蹭热点的噱头&#xff1f;我干了十二年Java后端&#xff0c;从Sp…

作者头像 李华
网站建设 2026/10/18 9:52:40

AI Agent核心模块拆解:从聊天机器人到数字员工的落地指南

开篇先聊一个现象&#xff1a;很多人做了大半年对话机器人&#xff0c;发现把模型 API 接上、套一层 Prompt、再挂个前端&#xff0c;得到的顶多是一个“即问即答的嘴巴”。它不会主动查数据、不会跨系统办手续、不会在流程跑偏时拉回来&#xff0c;更不会把一次复杂任务拆成若…

作者头像 李华
网站建设 2026/10/18 9:52:16

Linux常用命令实战指南:从文件操作到系统运维的高频用法

说真的&#xff0c;在图形界面越来越友好的今天&#xff0c;还有人愿意静下心来把玩 Linux 命令&#xff0c;本身就是件挺难得的事。前阵子帮一位刚入行的小兄弟梳理工作流&#xff0c;他问了我一句&#xff1a;“这些东西网上随手一搜就有&#xff0c;为什么还要系统学一遍&am…

作者头像 李华
网站建设 2026/10/18 9:50:43

本地部署视频分析工作流:抽帧、目标检测、OCR与API服务实战

视频分析是一个比“跑通一个模型”更完整的工程场景&#xff1a;要抽帧、要检测、要识别文字、要批量处理长视频&#xff0c;最后还要把结果落到结构化的位置。很多开源项目单看 demo 很漂亮&#xff0c;一上真实视频就暴露问题。这次我们直接从本地部署视角&#xff0c;整理一…

作者头像 李华