资讯动态

第12章:Spring AI 2.0文档与嵌入抽象

发布时间:2026/10/14 14:31:30 来源:尧图企业网站定制
上章思考题答案先查计数。计数是 1说明函数确实返回了「未发货」承诺来自第二次生成。系统提示词里若写了「可以说预计送达」模型会加戏。测试除了断言含「未发货」还要断言不含「明天送达」「预计」。没有记忆时两次提问就是两次独立调用工具执行两次。窗口记忆里即使留下了上一轮的「未发货」状态仍可能变化第二次默认还要再查。记忆适合记住订单号不适合记住会变的发货状态。1. 项目背景1.1 业务场景邻里仓的售后手册有三条短制度未发货可退、已发货不退、超时怎么算。用户问「货还没发出去钱能不能退回来」。这句话和手册里的「未发货可退」不是同一串字但说的是一件事。客服要先找出最相近的那一条再把原文说成口语。找的过程不能把整本手册都塞进聊天提示词三条还好三十条就会挤掉订单号。本章先把每条制度放进Document用本地的TransformersEmbeddingModel变成向量算两两相似度。聊天仍交给DeepSeekChatModel只负责把选中的那一条翻译成口语。DeepSeek 这个模块没有嵌入实现不要对https://api.deepseek.com发嵌入请求。三条制度分别是未发货可退、已发货不退、库存不足暂停发货。问句用口语「货还没发出去钱能不能退回来」它和第一条不是同一个字符串但应当更接近第一条而不是库存那一条。1.2 痛点用contains(退)做检索「不退」和「可退」会撞在一起。把三条都放进系统提示词模型有时引用已发货条款来回答未发货问题。有人以为DeepSeekChatModel既然能聊天就也能embed。这个模块只有/chat/completions。硬调一个不存在的嵌入路径得到的是 404不是向量。没有文档和嵌入时的路径关键词碰撞 → 可退和不退分不开或整本手册进提示词 → 引用错条款或向 DeepSeek 要向量 → 接口不存在。接口不存在时有人会把聊天模型的隐藏状态当成向量用。那不是稳定的嵌入换一次模型或温度分数就变了。嵌入必须走EmbeddingModel并且在本机算完。聊天只消费已经选好的那一条正文。三条 DocumentTransformersEmbeddingModel两两相似度最相近的一条DeepSeekChatModel 说口语2. 项目设计小胖说「这不就是把菜谱收成味道吗客人说想吃清淡的你不用把整本菜谱念完闻一闻哪页最像。把字数出来不就行了」小白问「味道是谁闻的DeepSeek 闻还是我们自己的机器闻闻的时候要不要把页眉页脚也算进去换了一台闻味道的机器以前的味道还能比吗」小胖又说「向量不就是一个数组吗长度不同也能比取前几个数就行。」小白追问「前几个数代表什么比完之后最像的那一条直接给用户还是再让聊天模型说一遍说的时候它会不会把『不退』说成『可退』」大师先划清两种模型「Document是一条制度和它的元数据例如条款编号R-01。EmbeddingModel把文本变成float[]和ChatModel不是同一个接口。TransformersEmbeddingModel默认用本机的 all-MiniLM-L6-v2维度是 384。构造之后要afterPropertiesSet()第一次会下载 ONNX 模型和分词器。MetadataMode决定嵌入或展示时带不带元数据文本NONE只嵌正文EMBED把元数据算进向量ALL和INFERENCE在格式化内容时使用。本章嵌入用NONE条款编号放在元数据里不放进向量文本避免编号本身把不相关的条款拉近。DeepSeek 只在选出条款之后把这一条正文改写成口语。」技术映射菜谱的一页是Document闻味道的是TransformersEmbeddingModel对着客人说人话的仍是DeepSeekChatModel。小白又问下载「第一次afterPropertiesSet要拉 ONNX。构建机不能上网时这条测试是失败还是跳过缓存目录里已经有文件还要不要再访问网络」小胖说「下不下来就改用随机向量测试先绿。」小白说「随机向量没有语义退款和库存的大小关系会翻转。绿了也是假的。不能为了构建通过换成随机数。」大师把环境和语义分开「缓存目录里已有模型和分词器时嵌入不再依赖网络。没有缓存且机器不能下载这道相似度测试应该失败并说明缺模型而不是改用随机向量假装通过。聊天密钥缺失只跳过口语那一条不跳过余弦比较。两件事的开关分开写。」小白问换模型「今天用 384 维明天换成另一个嵌入模型旧向量还能留在库里吗相似度是距离越小越好还是余弦越大越好」小胖说「都是数组拼在一起检索就行。」小白反对「维度不同点积的长度对不上。就算维度碰巧相同空间也不是同一个分数没有意义。」大师规定比较方式「本章用余弦相似度范围大约在 -1 到 1越大越像。三条制度两两比较未发货和『货还没发出去能不能退』应高于未发货和库存条款。不要把某一次的 0.82 写进断言。断言写相对关系问句与未发货条款的分数高于问句与库存条款的分数。换嵌入模型后旧向量全部作废必须重新嵌入。本章还没有向量库所以『重建索引』先表现为重新调用embed。真正的库在下一章。」技术映射换了一台闻味道的机器以前记下的味道不能混用。重建就是重新embed。小胖问口语「最像的那条已经是人话了还要再花一次 DeepSeek 吗」小白问「模型把 R-02 已发货不退说成可以退你怎么发现元数据里的条款编号要不要出现在口语里」大师把改写收紧「口语只复述选中的那一条不得引入另一条。提示词里放上条款编号和正文要求回答开头写上编号。测试断言编号还在并且未发货问题的回答含『可退』、不含『不予退款』。库存句子不要送给这次聊天。只送最相近的一条就是为了不给模型第二条款去发挥。」技术映射只把闻起来最像的那一页交给窗口念。窗口必须念出页码也就是条款编号。页码对了还要核对正文方向没有被说反。小白补了一句「三条都很短。短句的余弦差可能只有零点零几。差这么小还要不要送去口语」大师说「本章只证明相对顺序退款问句更接近未发货条款而不是库存条款。差很小就不要发明一个 0.8 的绝对门槛。绝对门槛是下一章检索的事。口语步骤只在相对顺序正确时才打开密钥。」3. 项目实战3.1 环境准备增加依赖spring-ai-transformers版本由 BOM 管理不要单独写版本号。它会带上 ONNX 运行时。第一次afterPropertiesSet()需要能访问模型地址随后可以setResourceCacheDirectory指到本地目录避免每条测试都下载。聊天密钥仍是DEEPSEEK_API_KEY。相似度测试不需要这个密钥。三条制度先写在代码里第 14 章再改成从文件读取。缓存目录不要放在源码树。放在构建输出目录测试结束可以删。构建机若完全不能访问模型地址应事先把 ONNX 放进缓存而不是把测试改成随机向量。随机向量会让「退款更近」这条关系失去意义构建绿了也不能说明嵌入可用。口语改写单独用环境变量开关缺密钥时跳过的只有那一条。3.2 分步实现步骤 1三条 Document步骤目标正文和条款编号分开存放。DocumentunshippedDocument.builder().text(未发货订单可以申请退款。).metadata(clauseId,R-01).build();DocumentshippedDocument.builder().text(已发货订单不予退款。).metadata(clauseId,R-02).build();DocumentstockDocument.builder().text(库存不足时暂停发货并通知用户。).metadata(clauseId,S-01).build();运行结果文字描述unshipped.getText()不含R-01。getMetadata().get(clauseId)为R-01。getFormattedContent(MetadataMode.NONE)只有正文。MetadataMode.ALL的格式化结果会带上元数据本章嵌入不要用它。可能的坑把「条款R-01未发货可退」整个写进正文。编号变成向量的一部分以后只问「R-01」也会命中但问法稍变时编号又会干扰相似度。编号放元数据。步骤 2本地嵌入并计算余弦步骤目标问句更接近未发货条款而不是库存条款。TransformersEmbeddingModelmodelnewTransformersEmbeddingModel(MetadataMode.NONE);model.setResourceCacheDirectory(/tmp/linlicang-onnx);model.afterPropertiesSet();float[]querymodel.embed(货还没发出去钱能不能退回来);float[]refundmodel.embed(unshipped);float[]inventorymodel.embed(stock);余弦用点积除以两个向量的模。三个向量长度都必须是model.dimensions()本章期望 384。长度不同就不要比直接失败。运行结果文字描述问句与未发货条款的余弦高于问句与库存条款的余弦。未发货条款与已发货条款也不会完全相同因为「可以申请退款」和「不予退款」方向不同。打印三个分数供人工看断言只写大小关系。可能的坑忘记afterPropertiesSet()第一次embed空指针。另一个坑是缓存目录不可写每次测试都重新下载构建变慢。目录指到本机可写路径。再一个坑是用 DeepSeek 的 base-url 加一个/embeddings。这个模块没有这条路径不要写这个客户端。步骤 3只把最相近的一条交给 DeepSeek步骤目标口语里出现 R-01并且保持可退。StringspokenchatClient.prompt().system(只根据给定条款回答。回答开头写条款编号。不要引入其他条款。).user(条款编号unshipped.getMetadata().get(clauseId)\n正文unshipped.getText()\n用户问题货还没发出去钱能不能退回来).call().content();选条款的逻辑是比较余弦不是把三条都拼进user。库存条款不得出现在这次用户消息里。运行结果文字描述回答以R-01开头或至少包含R-01包含「退」不包含「不予退款」也不包含S-01。这一次才使用DEEPSEEK_API_KEY。可能的坑为了省事把三条都放进提示词模型引用了 R-02。处理用户消息里只有胜出的那一条。另一个坑是嵌入用了MetadataMode.EMBED正文很短、元数据很长相似度被编号和来源路径支配。本章保持NONE。步骤 4换模型前先记下维度步骤目标让测试在维度变化时失败而不是悄悄截断。断言model.dimensions()等于每条embed结果的长度。把维度和模型资源 URI 打进日志。以后若换成别的 ONNX这条断言会红提醒重建全部向量。不要写一个「取前 384 维」的兼容层。运行结果文字描述维度为 384三条向量长度都是 384。可能的坑单元测试里把 384 写死同时又允许注入别的EmbeddingModel。注入模型变了断言应比较dimensions()和数组长度是否相等而不是永远等于 384。384 只作为默认模型的说明。相似度测试的输入用固定的三句话不要从生产日志里抽用户原话。原话里有地址和手机号既不该进测试仓库也会让分数随样本漂移。固定问句稳定之后才谈阈值。本章甚至还不设阈值只比较谁更大。下一章才会把「更大」收成检索请求。若这一章的大小关系在你机器上反了先检查是不是把库存正文写成了也含「退款」的句子而不是先怀疑余弦函数。正文要让条件词真正不同未发货、已发货、库存不足。口语那一次调用要打印选中的条款编号。编号不是 R-01 就不要看修辞好不好。修辞再顺条款错了也是失败。密钥只存在于环境变量口语测试用条件注解包起来。没有密钥时余弦测试仍要出现在默认构建里。3.3 完整代码清单PolicyDocuments、余弦函数、PolicySimilarityTest。源码对照spring-ai-commons/.../document/Document.javaspring-ai-commons/.../document/MetadataMode.javaspring-ai-model/.../embedding/EmbeddingModel.javamodels/spring-ai-transformers/.../TransformersEmbeddingModel.javamodels/spring-ai-deepseek下没有EmbeddingModel实现。3.4 测试验证TestvoidrefundQuestionIsCloserThanStock()throwsException{TransformersEmbeddingModelmodelnewTransformersEmbeddingModel(MetadataMode.NONE);model.afterPropertiesSet();float[]querymodel.embed(货还没发出去钱能不能退回来);doublerefundCosine.between(query,model.embed(未发货订单可以申请退款。));doublestockCosine.between(query,model.embed(库存不足时暂停发货并通知用户。));System.out.println(refundrefund stockstock);assertThat(refund).isGreaterThan(stock);}mvn -DtestPolicySimilarityTest test这条不需要 DeepSeek 密钥。口语改写另用环境变量开关。预期退款分数高于库存分数。若大小关系反了先打印三条正文确认库存那条没有写成也在讨论退款。正文条件不同而分数仍然颠倒再检查是不是MetadataMode把编号和文件路径嵌了进去。不要为了变绿去交换断言的两边。打印出来的三个分数留在测试输出里供人选择下一章的阈值本章的断言仍然只写大于关系。口语测试即使打开了密钥也只断言条款编号还在不断言整段客服话术。编号丢失时先看送进用户消息的是不是胜出的那一条再看系统提示词有没有要求写出编号。4. 项目总结4.1 优点与缺点对比项关键词 containsDocument 加本地嵌入「不退」和「可退」都含退向量方向不同计算发生在哪应用内存本机 ONNX是否调用 DeepSeek可以不调用嵌入不调用口语才调用换模型无关必须全部重嵌元数据混在字符串里和正文分开优点问句和条款不必字面相同。嵌入不消耗 DeepSeek 的聊天额度。条款编号留在元数据回答时可以强制带上。相对分数比绝对阈值更容易先写测试。聊天模型只看见一条条款减少串条款。缺点第一次要下载 ONNX构建环境要能访问该地址。短句的相似度不稳定不能把 0.8 当成制度。嵌入模型一换旧向量作废。本机 CPU 算向量批量大时会慢。模型仍可能在口语里把条款说反所以要断言编号和关键词。4.2 适用场景适用三条到几十条制度先做相似度而不是上数据库。需要向团队证明聊天接口和嵌入接口不是同一个。回答必须带条款编号。测试环境没有向量数据库。想在换嵌入模型前看清维度。不适用手册已经是几千条本机数组比不过专用向量库。下一章先用内存库再往后才是外部库。需要 DeepSeek 官方嵌入。这个模块没有不要伪造一条路径。4.3 注意事项afterPropertiesSet()必须调用缓存目录要可写。嵌入使用MetadataMode.NONE编号放元数据。不要断言某一个余弦小数。换模型时删除旧向量不要截断或补零。4.4 常见踩坑未发货问题命中了已发货条款。根因是两条正文都太短或者正文只写了「退款」两个字。处理把条件写进正文例如「未发货」和「已发货」再比相对分数。构建在下载模型时超时。根因是没有缓存目录每次都远程拉取。处理设置setResourceCacheDirectory。口语把不予退款说成可以退。根因是提示词里放了两条相反条款。处理只送胜出的一条并断言否定词不出现。4.5 思考题MetadataMode.EMBED把clauseId算进向量之后只输入「R-01」可能比输入「能不能退」更接近这条文档。你希望编号能被单独检索到还是希望只有正文语义能命中本章该用哪一种模式相似度最高的一条只有 0.31下一条是 0.29。仍然把 0.31 送去改写口语会有什么问题阈值应该比「谁最大」多做哪一次判断答案在第 13 章开头。延伸阅读与资源10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑