ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python商品RAG实战:Embedding+SQLite实现关键词与向量混合检索

Python商品RAG实战:Embedding+SQLite实现关键词与向量混合检索 前言Function Calling解决了大模型与Python工具之间的结构化通信问题但模型仍然需要先找到与用户需求相关的商品。只使用WHERE product_name LIKE %关键词%可以处理“查询足球”这样的明确问题却难以理解“我准备跑马拉松需要透气并且能支撑足弓的装备”这类自然语言需求。本文基于电商客服AI Agent的真实代码使用text-embedding-v3、JSON向量索引、余弦相似度和中文关键词匹配实现轻量混合RAG。语义检索负责理解需求关键词检索负责保留商品名和品牌等精确信号两种分数融合后返回Top-K候选商品。文章还会重点解释一个容易被忽略的边界RAG结果只是候选不是价格和库存的权威证据。候选商品必须继续通过SQLite按商品ID验证才能进入最终回答。文章目录前言项目效果与流程图一、为什么商品查询需要RAG二、RAG的三个阶段三、RAG相关目录结构四、Embedding配置embedding_modelembedding_dimensionstop_k五、调用Embedding接口输入返回值调试提示六、商品如何转换为检索文档七、从SQLite读取商品八、构建商品向量索引九、运行索引构建脚本十、余弦相似度十一、中文关键词评分十二、混合检索核心有向量索引时没有向量索引时top_k限制十三、为什么is_verified_fact是False十四、将检索器包装为工具十五、离线假向量测试十六、完整运行步骤十七、断点调试建议十八、常见问题与解决方法1. 找不到product_embeddings.json2. 没有索引时程序是否完全不可用3. Embedding接口连接失败4. 向量维度不是10245. 商品数量和向量数量不一致6. 精确商品名排序不靠前7. 语义需求召回错误商品8. top_k传入0或很大数值9. JSON索引文件过大10. 修改价格后是否需要重建索引11. RAG结果能否直接回答库存12. 测试通过是否代表真实模型准确十九、性能与工程优化二十、总结项目效果与流程图商品RAG检索流程。用户问题先转换成查询向量再与商品索引进行混合评分最终返回候选商品ID。混合检索分数构成。存在向量索引时语义分数占65%关键词分数占35%。商品向量索引结构。每条记录保存商品ID、检索文档和1024维向量。检索与验证的边界。RAG负责召回候选SQLite负责确认商品事实。一、为什么商品查询需要RAG传统名称查询通常是SELECT*FROMproductsWHEREproduct_nameLIKE%足球%;它适合用户明确说出标准商品名但存在三个限制用户可能只描述用途不说商品名。同一种需求可以有多种自然语言表达。商品描述、规格和使用场景中的信息没有被充分利用。例如用户提问我想参加长距离跑步需要透气并且能支撑足弓的产品。数据库中的标准名称是“跑步鞋”。如果只匹配用户原句中的商品名称查询可能没有结果。Embedding可以把用户需求和商品描述转换到相同的向量空间使语义相近的文本获得较高相似度。二、RAG的三个阶段RAG是Retrieval-Augmented Generation即检索增强生成。Retrieval检索相关商品 ↓ Augmentation把检索结果加入模型上下文 ↓ Generation模型基于上下文生成回答本项目进一步增加SQL验证用户问题 → 混合RAG召回候选商品ID → SQLite按ID查询权威记录 → 模型基于验证结果回答因此项目中的RAG并不直接承担最终事实输出。三、RAG相关目录结构function_calling_rag_agent/ │ ├── config.py ├── llm_client.py ├── database.py │ ├── data/ │ ├── products.db │ └── product_embeddings.json │ ├── rag/ │ ├── __init__.py │ ├── retriever.py │ └── build_index.py │ ├── tools/ │ └── product_tools.py │ └── tests/ └── test_retriever.py文件职责如下文件职责config.py保存数据库、索引路径和Embedding配置llm_client.py调用百炼Embedding接口retriever.py构建索引、计算相似度和混合评分build_index.py执行真实向量索引构建product_tools.py将检索器包装为Function Calling工具test_retriever.py使用假向量离线验证检索流程四、Embedding配置项目配置fromdataclassesimportdataclassfrompathlibimportPath PROJECT_ROOTPath(__file__).resolve().parent DATABASE_PATHPROJECT_ROOT/data/products.dbINDEX_PATHPROJECT_ROOT/data/product_embeddings.jsondataclass(frozenTrue)classSettings:base_url:strhttps://dashscope.aliyuncs.com/compatible-mode/v1chat_model:strqwen-plusembedding_model:strtext-embedding-v3embedding_dimensions:int1024max_iterations:int10top_k:int3SETTINGSSettings()embedding_modeltext-embedding-v3负责把文本转换成向量。embedding_dimensions1024每段商品文本会转换为长度1024的浮点数列表。向量维度不是商品数量也不是词语数量而是模型用于表达文本语义特征的数值空间。top_k默认返回3个候选商品。候选太少可能漏掉相关结果太多则会增加后续SQL查询和模型上下文内容。五、调用Embedding接口defembed_texts(client:OpenAI,texts:list[str])-list[list[float]]:responseclient.embeddings.create(modelSETTINGS.embedding_model,inputtexts,dimensionsSETTINGS.embedding_dimensions,)return[item.embeddingforiteminresponse.data]输入texts:list[str]可以一次传入多条商品文档减少逐条请求造成的网络往返。返回值list[list[float]]外层列表对应输入文本内层列表是每条文本的1024维向量。调试提示建议在返回前观察len(texts) len(response.data) len(response.data[0].embedding)正常情况下输入数量与返回数量一致单条向量长度为1024。不要打印全部向量否则控制台会出现大量浮点数。六、商品如何转换为检索文档product_document()把SQLite的一条商品记录拼成自然语言defproduct_document(product:dict)-str:return(f商品ID{product[product_id]}f商品名称{product[product_name]}f品牌{product[brand]}f描述{product[description]}f规格{product[specifications]}f适用场景{product[usage]})价格和库存没有放进检索文档。原因是价格和库存属于可能变化的业务事实最终回答必须重新查询SQLite而不是依赖旧向量索引中的文本。以跑步鞋为例文档类似商品ID004商品名称跑步鞋品牌阿迪达斯 描述适合长距离跑步舒适透气提供良好的足弓支撑 规格多种尺码透气网布适用场景长跑、日常训练文档字段越贴近用户真实表达语义检索越容易召回相关商品。七、从SQLite读取商品defload_products()-list[dict]:withconnect()asconnection:rowsconnection.execute(SELECT * FROM products ORDER BY product_id).fetchall()returnrows_to_dicts(rows)返回结构[{product_id:004,product_name:跑步鞋,description:适合长距离跑步...,specifications:多种尺码透气网布,usage:长跑、日常训练,brand:阿迪达斯,price:500.0,stock_quantity:20}]检索器读取全部商品用于构建轻量索引。当前只有10条数据这种方式足够直观商品数量很大时应考虑增量索引和专业检索服务。八、构建商品向量索引defsave_embedding_index(embedder,index_pathINDEX_PATH)-int:productsload_products()documents[product_document(product)forproductinproducts]vectorsembedder(documents)payload[{product_id:product[product_id],document:document,embedding:vector,}forproduct,document,vectorinzip(products,documents,vectors,strictTrue)]index_path.parent.mkdir(parentsTrue,exist_okTrue)index_path.write_text(json.dumps(payload,ensure_asciiFalse),encodingutf-8)returnlen(payload)zip(..., strictTrue)要求商品、文档和向量数量完全一致。如果Embedding接口少返回一条程序会及时报错避免商品ID与错误向量对应。索引最终保存为JSON[{product_id:004,document:商品ID004商品名称跑步鞋...,embedding:[0.012,-0.038,0.104]}]示例只展示少量数值真实向量包含1024个浮点数。九、运行索引构建脚本rag/build_index.pyfromllm_clientimportcreate_client,embed_textsfromrag.retrieverimportsave_embedding_indexif__name____main__:clientcreate_client()countsave_embedding_index(lambdatexts:embed_texts(client,texts))print(f向量索引构建完成共写入{count}条商品记录。)运行python rag/build_index.py项目实际构建结果为向量索引构建完成共写入10条商品记录。修改商品名称、描述、规格、品牌或用途后需要重新构建索引。只修改价格或库存时不需要因为这两个字段不在检索文档中。十、余弦相似度def_cosine_similarity(left:list[float],right:list[float])-float:numeratorsum(a*bfora,binzip(left,right,strictTrue))left_normmath.sqrt(sum(value*valueforvalueinleft))right_normmath.sqrt(sum(value*valueforvalueinright))ifleft_norm0orright_norm0:return0.0returnnumerator/(left_norm*right_norm)公式为cosine(A, B) A·B / (||A|| × ||B||)通俗理解它比较两个向量的方向是否接近。用户问题与某个商品文档语义越接近余弦相似度通常越高。当前项目使用Python标准库计算没有依赖NumPy。数据量只有10条时性能足够大规模商品库不适合逐条Python计算。十一、中文关键词评分def_lexical_score(query:str,document:str)-float:normalized_queryre.sub(r\s,,query.lower())normalized_documentre.sub(r\s,,document.lower())ifnotnormalized_query:return0.0tokensset(normalized_query)tokens.update(normalized_query[index:index2]forindexinrange(len(normalized_query)-1))matchessum(1fortokenintokensiftokenandtokeninnormalized_document)exact_bonus2ifnormalized_queryinnormalized_documentelse0returnmin(1.0,(matchesexact_bonus)/max(len(tokens),1))这段代码生成两类Token单个字符例如“跑”“步”“鞋”。连续两个字符例如“跑步”“步鞋”。二元词组比单字更能保留中文局部语义。若完整查询直接出现在商品文档中再增加精确匹配奖励。这不是专业中文分词器而是适合小型教学项目的轻量实现。十二、混合检索核心classHybridProductRetriever:def__init__(self,embedderNone,index_pathINDEX_PATH):self.embedderembedder self.index_pathindex_pathdefsearch(self,query:str,top_k:int3)-list[dict]:productsload_products()semantic_scores{}ifself.embedderandself.index_path.exists():indexjson.loads(self.index_path.read_text(encodingutf-8))query_vectorself.embedder([query])[0]semantic_scores{item[product_id]:_cosine_similarity(query_vector,item[embedding])foriteminindex}results[]forproductinproducts:documentproduct_document(product)lexical_lexical_score(query,document)semanticsemantic_scores.get(product[product_id],0.0)combined(0.65*semantic0.35*lexicalifsemantic_scoreselselexical)ifcombined0:results.append({product_id:product[product_id],product_name:product[product_name],retrieval_score:round(combined,4),matched_document:document,is_verified_fact:False,})results.sort(keylambdaitem:item[retrieval_score],reverseTrue)returnresults[:max(1,min(top_k,10))]有向量索引时combined 0.65 × semantic 0.35 × lexical语义相似度权重更高关键词分数用于保留精确名称和品牌信号。没有向量索引时combined lexical检索器自动降级为关键词模式基本查询仍然可用但自然语言需求理解能力会下降。top_k限制results[:max(1,min(top_k,10))]保证至少返回1条、最多返回10条防止模型传入异常值导致大量候选进入上下文。十三、为什么is_verified_fact是False候选结果包含is_verified_fact:False它明确提醒模型和程序这条记录只是检索候选 不等于价格、库存、品牌已经验证search_products工具还会返回警告{warning:(候选结果不是权威事实必须用get_product_details进行SQL验证。)}RAG负责找到相关商品下一步必须根据product_id查询SQLite。十四、将检索器包装为工具defsearch_products(retriever:HybridProductRetriever,query:str,top_k:int3)-dict:candidatesretriever.search(queryquery,top_ktop_k)return{status:(candidates_foundifcandidateselsenot_found),source:hybrid_rag_index,warning:(候选结果不是权威事实必须用get_product_details进行SQL验证。),candidates:candidates,}Function Calling模型只接触这个工具返回的JSON不需要了解余弦相似度和索引文件的内部实现。十五、离线假向量测试真实Embedding接口需要网络和API。测试使用确定性假向量deffake_embedder(texts:list[str])-list[list[float]]:vectors[]fortextintexts:vectors.append([float(长跑intextor跑步intext),float(足球intext),float(瑜伽intext),])returnvectors测试代码deftest_semantic_retrieval_returns_running_shoes(self):withtempfile.TemporaryDirectory()asdirectory:index_pathPath(directory)/index.jsonsave_embedding_index(fake_embedder,index_path)retrieverHybridProductRetriever(fake_embedder,index_path)resultsretriever.search(我想长跑需要透气和足弓支撑,top_k1)self.assertEqual(results[0][product_id],004)self.assertFalse(results[0][is_verified_fact])该测试验证自然语言需求能够召回跑步鞋。Top-1商品ID为004。检索结果仍被标记为未验证事实。它不证明真实Embedding模型的准确率也没有计算召回率等指标。十六、完整运行步骤进入新版项目目录。cdD:\Jupyter_Projects\PythonProject\大模型学习\AI Agent\function_calling_rag_agent初始化数据库。python scripts/initialize_database.py构建真实向量索引。python rag/build_index.py运行离线检索测试。python-munittest tests.test_retriever-v启动完整客服Agent。python main.py输入语义需求。我想进行长距离跑步需要透气并且能支撑足弓的商品。十七、断点调试建议推荐断点documents [...]查看商品检索文档。vectors embedder(documents)检查向量数量。query_vector self.embedder([query])[0]检查查询向量。_cosine_similarity(...)比较单个候选分数。combined ...观察两种分数融合。results.sort(...)查看排序前后结果。关键变量query documents query_vector semantic_scores lexical semantic combined results不要在调试器中展开全部1024维向量优先观察长度、前5个数值和最终相似度。十八、常见问题与解决方法1. 找不到product_embeddings.json原因尚未运行索引构建脚本或索引路径错误。解决运行python rag/build_index.py并检查INDEX_PATH。2. 没有索引时程序是否完全不可用原因误以为RAG只能依赖向量。解决当前代码会降级到关键词评分但语义召回能力会下降。3. Embedding接口连接失败原因网络权限、接口地址或系统环境变量存在问题。解决检查网络和DASHSCOPE_API_KEY不要打印密钥。4. 向量维度不是1024原因模型配置、dimensions参数或旧索引不一致。解决确认配置后重新构建完整索引。5. 商品数量和向量数量不一致原因Embedding返回不完整或构建过程中数据变化。解决zip(strictTrue)会报错应重新构建而不是忽略差异。6. 精确商品名排序不靠前原因语义分数占比较高关键词特征不足。解决检查lexical分数按真实测试调整权重不要凭感觉修改。7. 语义需求召回错误商品原因商品描述过短、字段缺失或需求表达模糊。解决完善描述、规格和适用场景并建立带标准答案的评测集合。8. top_k传入0或很大数值原因模型参数可能异常。解决当前代码将结果限制在110条Schema也定义相同范围。9. JSON索引文件过大原因商品数量和向量维度增加。解决当前方案适合小数据教学大规模场景需要专业向量存储。10. 修改价格后是否需要重建索引原因不清楚哪些字段进入检索文档。解决价格和库存未写入检索文档无需重建描述、规格等变化需要重建。11. RAG结果能否直接回答库存原因混淆候选召回和事实验证。解决不能。必须调用get_product_details查询SQLite。12. 测试通过是否代表真实模型准确原因离线测试使用了人为设计的三维假向量。解决它只验证程序流程真实效果需要单独构建查询集和召回指标。十九、性能与工程优化商品数据量增加时改为增量索引。批量调用Embedding减少网络往返。缓存高频查询向量。为索引增加版本和生成时间元数据。商品更新后只重建受影响记录。建立真实查询—商品ID评测集合。使用RecallK、MRR等检索指标评估权重。大规模数据迁移到专业向量数据库。对索引文件进行完整性校验。把检索耗时和候选分数写入调试日志。这些属于后续工程化方向当前项目没有实现相应性能指标或专业向量数据库。二十、总结本文完成了一个轻量商品混合RAG检索器。项目将商品名称、品牌、描述、规格和适用场景组成检索文档使用text-embedding-v3生成1024维向量并将向量保存到JSON索引。查询时用户问题同样转换为向量程序使用余弦相似度得到语义分数同时通过中文字符和二元词组计算关键词分数。存在向量索引时最终分数由65%语义分数和35%关键词分数组成索引不可用时则降级为关键词检索。最重要的设计边界是RAG只返回候选商品ID并明确标记is_verified_factFalse。价格、库存、品牌和优惠必须继续通过SQLite验证。下一篇将分析SQL复核、Decimal价格计算、EvidenceLedger和最终回答拦截如何共同降低模型幻觉。
返回列表