最近在给一个旅游项目做问答机器人需求很直白游客问“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跑本地大模型生成回答。全程数据不出内网断网也能用适合要求私有化部署的场景。云上方案用的是阿里云百炼平台它把向量模型、大模型、知识库管理都做成APISpring AI通过一个starter就能对接适合快速迭代、不想折腾GPU的环境。两个方案背后的业务逻辑、Prompt模板、文档拆分策略是完全一致的只是依赖配置和几个Bean的实现不同这正是Spring AI框架最舒服的地方。2. Spring AI 项目搭建与核心概念2.1 工程初始化我用的Spring Boot 3.2 Spring AI 0.8.1Java 17。Spring AI目前还在快速迭代版本号变化大建议以当前Maven中央仓库发布的最新RELEASE为准。建工程时直接加上核心依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-core/artifactId version0.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、ChatModelSpring AI对LLM生态做了很好的抽象核心就三个接口理解这三个就理解了整个框架EmbeddingModel负责把文本转成向量数组本地方案用OllamaEmbeddingModel或ONNXEmbeddingModel云上方案用百炼的实现VectorStore负责向量的增删查本地可以用PgVectorStore或ChromaVectorStoreChatModel负责对话生成本地用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多MBCPU就能跑对几十万文档的规模完全够用。如果你不想自己下模型也可以用Ollama拉取nomic-embed-text再通过OllamaEmbeddingModel调用但注意Ollama的Embedding模型对中文支持参差不齐建议实测后再定。本地有GPU的话可以换bge-large-zh效果更好但内存占用和推理时间都会上去。做向量化时有一个细节维度。BGE小模型的维度是512PGVector建表时必须指定维度而且要跟模型维度一致否则存数据直接报错。我建表用的语句是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 ListDocument splitDocuments(ListDocument docs) { ListDocument 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) { ListDocument 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); }这里topK5是我试出来的平衡值。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仅需网络数据隐私数据完全内网闭环知识库数据上传至云端中文效果依赖选的中文模型BGEQwen效果不错默认效果好尤其长文档理解首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继续用PGVectorChatModel换百炼的。唯一要注意的是百炼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_PARALLEL4。如果机器内存足够也可以换更大的模型效果和速度都会更好。还有一点不要让Ollama和PGVector跑在同一台2C4G的小机器上否则内存紧张会导致系统OOM建议至少8G内存起步。6.5 安全合规与API Key管理这个是很多人容易忽略的。代码仓库里一旦提交了API-KEY基本等同于泄露。我在项目里用了Spring Boot的ConfigurationProperties配合环境变量本地开发用.env生产环境用密钥管理服务。另外面向用户开放的问答接口一定要接入频率限制防止有人用你的接口免费刷大模型导致账单爆炸。旅游问答还好一旦涉及敏感信息还要在检索结果后做一轮内容过滤把不适宜展示的内容拦掉。7. 写在最后的一些体会这一套从零到上线做下来我最大的感受是RAG的技术栈并没有多神秘真正难的是把文档整理得适合检索以及把检索结果和Prompt设计打磨到和业务匹配。Spring AI帮我把大部分样板代码省掉了让我可以在一套代码里随意切换本地和云端模型这个抽象价值非常大。最后分享一个小技巧在similaritySearch返回结果时不要把原始片段直接拼给大模型最好在片段前面加上“来源某文档某段落”让大模型在回答时把引用一并带上。我后来加了这么一段业务方看到答案时能直接跳转到原文出处整个系统的可信度一下子就不一样了。如果你的旅游知识库后续也要做多客服、多语种支持RAG这条路完全可以继续延伸下去核心链路是不用动的。