大模型客服如何防幻觉:RAG+SQL双重验证价格、库存和优惠
前言电商客服最危险的错误不是回答不够流畅而是编造价格、库存、品牌和优惠。即使已经接入RAG模型仍可能把相似商品当成目标商品、修改检索结果中的数字或者在没有数据时根据常识补充一个看似合理的答案。本文基于Function CallingRAG电商客服项目分析一套程序级证据链RAG只召回候选商品IDSQLite按ID验证商品与促销Decimal根据数据库规则计算价格EvidenceLedger记录允许进入回答的事实validate_answer()在输出前检查商品ID和人民币金额。这套方案的目标是降低幻觉和拦截无证据回答并不意味着能够100%消除所有模型错误。文章将同时说明已实现能力、测试覆盖和当前边界。文章目录前言项目防幻觉效果与结构一、电商客服常见的模型幻觉编造不存在的商品修改真实价格忽略优惠条件混淆赠品和降价二、为什么RAG仍然可能幻觉三、防幻觉相关目录四、SQLite数据库结构五、数据库连接封装六、RAG结果为什么不是权威事实七、商品SQL验证工具输入限制返回状态八、促销SQL验证工具九、促销类型和条件十、为什么价格工具不接收单价十一、Decimal确定性价格计算十二、价格工具返回证据十三、EvidenceLedger证据账本十四、数字规范化十五、最终答案校验器十六、Agent如何串联证据与校验十七、自动化测试覆盖十八、运行与调试十九、常见问题与解决方法1. RAG找到商品但答案被拦截2. 数据库有商品却返回not_found3. 优惠没有应用4. 120元被错误拦截5. ¥999没有被识别6. 回答没有商品ID7. promotion状态verified但商品未验证8. 赠品优惠导致价格错误降低9. 买三送一数量理解不一致10. SQL占位符数量错误11. 模型使用“售价一百二十元”12. 测试通过是否表示零幻觉二十、总结项目防幻觉效果与结构电商客服四层防幻觉流程。候选召回、SQL复核、确定性计算和答案校验依次执行。EvidenceLedger证据账本。最终回答只能引用工具记录过的商品ID和金额。确定性价格计算流程。模型不能把自己生成的单价作为可信输入。最终回答校验分支。证据不足时返回安全兜底回答。一、电商客服常见的模型幻觉编造不存在的商品数据库没有无人机模型却回答本店有登山无人机目前售价999元库存20台。修改真实价格数据库中的足球价格是120元模型可能输出足球现在售价99元。忽略优惠条件羽毛球拍需要购买两支以上才享受8折模型却把购买一支也算成240元。混淆赠品和降价瑜伽垫优惠是赠送教程视频不代表商品价格减少50元。这些错误不能只依靠Prompt中的“不要编造”解决。Prompt是行为引导不是程序级验证。二、为什么RAG仍然可能幻觉RAG改善的是信息获取不保证最终回答必然正确。可能出现向量检索召回相似但错误的商品。向量索引未及时更新。模型忽略检索结果中的条件。模型在组织语言时修改数字。没有检索结果时仍根据常识回答。因此项目采用RAG负责“可能是谁” SQLite负责“事实是什么” Decimal负责“价格怎么算” Validator负责“答案能否放行”三、防幻觉相关目录function_calling_rag_agent/ │ ├── database.py ├── agent.py │ ├── tools/ │ ├── product_tools.py │ ├── price_tools.py │ └── registry.py │ ├── guardrails/ │ ├── evidence.py │ └── validator.py │ ├── scripts/ │ └── initialize_database.py │ └── tests/ └── test_tools_and_guardrails.py文件职责database.py统一SQLite连接和行转换product_tools.py商品、促销权威查询price_tools.py重新查询数据并计算价格evidence.py记录已验证ID和数字validator.py校验最终回答test_tools_and_guardrails.py验证正常与虚假场景四、SQLite数据库结构商品表CREATETABLEproducts(product_idTEXTPRIMARYKEY,product_nameTEXTNOTNULL,descriptionTEXTNOTNULL,specificationsTEXTNOTNULL,usageTEXTNOTNULL,brandTEXTNOTNULL,priceREALNOTNULLCHECK(price0),stock_quantityINTEGERNOTNULLCHECK(stock_quantity0));促销表CREATETABLEpromotions(promotion_idTEXTPRIMARYKEY,product_idTEXTNOTNULLREFERENCESproducts(product_id),promotion_textTEXTNOTNULL,discount_typeTEXTNOTNULL,discount_valueREAL,minimum_quantityINTEGERNOTNULLDEFAULT1,minimum_spendREAL,extra_conditionTEXT);product_id是整条证据链的连接点RAG候选商品ID → products商品事实 → promotions优惠规则 → 确定性价格计算 → 最终回答引用五、数据库连接封装importsqlite3fromcontextlibimportcontextmanagerfromconfigimportDATABASE_PATHcontextmanagerdefconnect():connectionsqlite3.connect(DATABASE_PATH)connection.row_factorysqlite3.Rowtry:yieldconnectionfinally:connection.close()defrows_to_dicts(rows):return[dict(row)forrowinrows]row_factorysqlite3.Row让查询结果既可以按位置访问也能转换为带字段名的字典。contextmanager确保工具执行完成或抛出异常后连接都能关闭。调试时观察DATABASE_PATH connection.row_factory dict(row)六、RAG结果为什么不是权威事实search_products()返回{status:candidates_found,source:hybrid_rag_index,warning:(候选结果不是权威事实必须用get_product_details进行SQL验证。),candidates:[{product_id:001,product_name:足球,retrieval_score:0.91,is_verified_fact:False}]}向量相似度只能说明文本相关不能证明价格、库存和品牌当前有效。下一步必须调用get_product_details()。七、商品SQL验证工具defget_product_details(product_ids:list[str])-dict:unique_idslist(dict.fromkeys(str(item)foriteminproduct_ids))[:10]placeholders,.join(?for_inunique_ids)withconnect()asconnection:rowsconnection.execute(f SELECT * FROM products WHERE product_id IN ({placeholders}) ORDER BY product_id ,unique_ids,).fetchall()productsrows_to_dicts(rows)return{status:verifiedifproductselsenot_found,source:sqlite.products,products:products,missing_product_ids:sorted(set(unique_ids)-{item[product_id]foriteminproducts}),}输入限制商品ID统一转换成字符串。使用dict.fromkeys()去重并保留顺序。最多处理10个商品ID。SQL值使用?占位符。返回状态查到商品status verified source sqlite.products没有查到status not_found模型不能把not_found解释成“可能有货”。八、促销SQL验证工具defget_promotions(product_ids:list[str])-dict:unique_idslist(dict.fromkeys(str(item)foriteminproduct_ids))[:10]placeholders,.join(?for_inunique_ids)withconnect()asconnection:rowsconnection.execute(f SELECT * FROM promotions WHERE product_id IN ({placeholders}) ORDER BY promotion_id ,unique_ids,).fetchall()promotionsrows_to_dicts(rows)return{status:verifiedifpromotionselsenot_found,source:sqlite.promotions,promotions:promotions,}促销不再通过商品名称读取文本而是按商品ID关联避免“足球”同时出现在其他商品描述中造成误匹配。九、促销类型和条件项目包含类型示例计算方式percentage足球9折小计乘0.9fixed_reduction满500减100小计减100buy_x_get_y买三送一根据数量计算收费件数gift赠送教程或会员不降低商品价格free_shipping满200包邮不修改商品小计条件字段包括minimum_quantityminimum_spendextra_condition模型必须明确说明条件是否满足不能只看到“8折”就直接计算。十、为什么价格工具不接收单价Function Calling Schema只让模型传{product_id:001,quantity:1,is_first_purchase:false}模型不能传入{price:99}价格工具根据product_id重新查询SQLiteproductconnection.execute( SELECT product_id, product_name, price, stock_quantity FROM products WHERE product_id ? ,(str(product_id),),).fetchone()因此模型自己编造的单价不会进入计算过程。十一、Decimal确定性价格计算unit_priceDecimal(str(product[price]))subtotalunit_price*quantity final_totalsubtotal百分比折扣ifcondition_metanddiscount_typepercentage:final_totalsubtotal*discount_value满减elifcondition_metanddiscount_typefixed_reduction:final_totalmax(Decimal(0),subtotal-discount_value)金额统一保留两位小数def_money(value:Decimal)-float:returnfloat(value.quantize(Decimal(0.01),roundingROUND_HALF_UP))相比旧版eval()这套实现只执行明确允许的优惠规则不会执行任意Python表达式。十二、价格工具返回证据return{status:verified,source:[sqlite.products,sqlite.promotions,deterministic_decimal_calculation],product_id:product[product_id],product_name:product[product_name],quantity:quantity,unit_price:_money(unit_price),subtotal:_money(subtotal),final_total:_money(final_total),promotion_applied:applied,promotion_note:note,}它不只返回108还说明108来自哪个商品、多少件、什么优惠和哪些数据源。十三、EvidenceLedger证据账本classEvidenceLedger:def__init__(self)-None:self.verified_product_ids:set[str]set()self.allowed_numbers:set[str]set()self.has_verified_product_dataFalse三个核心状态属性含义verified_product_idsSQL或价格工具确认过的商品IDallowed_numbers工具结果中出现的合法数字has_verified_product_data是否获得过权威商品记录记录工具结果defrecord(self,tool_name:str,result:dict)-None:ifresult.get(status)!verified:returniftool_nameget_product_details:productsresult.get(products,[])self.has_verified_product_databool(products)forproductinproducts:self.verified_product_ids.add(str(product[product_id]))self._record_numbers(product)eliftool_namein{get_promotions,calculate_final_price}:self._record_numbers(result)ifresult.get(product_id):self.verified_product_ids.add(str(result[product_id]))search_products状态不是verified因此RAG候选不会直接进入权威证据集合。十四、数字规范化回答可能写成120元 120.00元 ¥120 120.0它们应被视为相同金额。项目使用Decimal.normalize()统一格式defnormalize_number(value)-str:try:numberDecimal(str(value))exceptInvalidOperation:returnstr(value)normalizedformat(number.normalize(),f)return0ifnormalizedin{-0,}elsenormalized例如120.00 → 120 108.0 → 108十五、最终答案校验器安全兜底回答SAFE_FALLBACK(当前商品数据库中没有足够的已验证信息来回答这个问题请联系人工客服进一步核实。)校验商品证据needs_product_evidenceany(wordinf{question}\n{answer}forwordinproduct_fact_words)ifneeds_product_evidenceandnotevidence.has_verified_product_data:returnFalse,SAFE_FALLBACK校验商品IDcited_idsset(re.findall(r商品ID[:]\s*([A-Za-z0-9_-]),answer))ifevidence.has_verified_product_data:ifnotcited_idsornotcited_ids.issubset(evidence.verified_product_ids):returnFalse,SAFE_FALLBACK校验人民币金额money_matchesre.findall(r[¥]\s*(\d(?:\.\d)?)|(\d(?:\.\d)?)\s*元,answer)money_values[prefix_valueorsuffix_valueforprefix_value,suffix_valueinmoney_matches]ifany(normalize_number(value)notinevidence.allowed_numbersforvalueinmoney_values):returnFalse,SAFE_FALLBACK模型回答999元或¥999.00时如果999没有出现在证据集合中回答就不会放行。十六、Agent如何串联证据与校验evidenceEvidenceLedger()for_inrange(SETTINGS.max_iterations):completionself.client.chat.completions.create(...)messagecompletion.choices[0].messageifmessage.tool_calls:fortool_callinmessage.tool_calls:resultself.registry.execute(tool_call.function.name,tool_call.function.arguments,)evidence.record(tool_call.function.name,result)messages.append({role:tool,tool_call_id:tool_call.id,name:tool_call.function.name,content:json.dumps(result,ensure_asciiFalse),})continueanswermessage.contentorSAFE_FALLBACK _,safe_answervalidate_answer(question,answer,evidence)returnsafe_answer候选回答生成后不会直接打印而是先进入validate_answer()。十七、自动化测试覆盖SQL商品验证resultget_product_details([001])self.assertEqual(result[status],verified)self.assertEqual(result[products][0][price],120.0)足球九折resultcalculate_final_price(001,1)self.assertEqual(result[final_total],108.0)self.assertTrue(result[promotion_applied])首次购买条件normalcalculate_final_price(004,1,is_first_purchaseFalse)firstcalculate_final_price(004,1,is_first_purchaseTrue)self.assertEqual(normal[final_total],500.0)self.assertEqual(first[final_total],400.0)虚假价格拦截invalid,fallbackvalidate_answer(足球多少钱,足球商品ID001价格999元。,evidence)self.assertFalse(invalid)self.assertEqual(fallback,SAFE_FALLBACK)项目完整测试集当前共9项通过其中防幻觉测试覆盖无证据商品、虚假数字、人民币符号和优惠条件等场景。十八、运行与调试运行测试cdD:\Jupyter_Projects\PythonProject\大模型学习\AI Agent\function_calling_rag_agentpython-munittest discover-stests-v推荐断点get_product_details()执行SQL后。calculate_final_price()读取商品和促销后。condition_met计算后。evidence.record()入口。validate_answer()提取商品ID后。人民币金额正则匹配后。重点观察products promotion subtotal final_total condition_met evidence.verified_product_ids evidence.allowed_numbers cited_ids money_values十九、常见问题与解决方法1. RAG找到商品但答案被拦截原因只调用了search_products没有调用get_product_details。解决检查System Prompt和工具消息确保候选ID继续经过SQL验证。2. 数据库有商品却返回not_found原因商品ID类型、数据库路径或数据初始化存在问题。解决检查DATABASE_PATH和实际表内容。3. 优惠没有应用原因数量、金额或首次购买条件不满足。解决观察minimum_quantity、minimum_spend、extra_condition和condition_met。4. 120元被错误拦截原因数字格式没有统一。解决所有工具数字和回答数字都通过normalize_number()处理。5. ¥999没有被识别原因金额正则只匹配了“元”后缀。解决当前代码同时匹配元、¥和修改时保留回归测试。6. 回答没有商品ID原因模型没有遵守System Prompt。解决校验器会返回兜底回答同时强化Prompt中的引用格式。7. promotion状态verified但商品未验证原因优惠记录不能替代商品详情验证。解决has_verified_product_data只由get_product_details设置。8. 赠品优惠导致价格错误降低原因把赠品价值误当作减价。解决gift只设置promotion_applied不修改final_total。9. 买三送一数量理解不一致原因用户所说数量可能是购买数量或最终获得数量。解决界面和工具Schema应进一步明确数量语义这是当前可改进点。10. SQL占位符数量错误原因空商品ID列表导致IN ()。解决Function Schema要求至少一个ID程序端仍可增加空列表提前返回。11. 模型使用“售价一百二十元”原因当前校验器主要识别阿拉伯数字金额。解决可增加中文数字规范化这是当前尚未实现的边界。12. 测试通过是否表示零幻觉原因自动化测试只能覆盖已设计场景。解决不能声称零幻觉应持续收集真实失败案例并增加回归测试。二十、总结本文分析了电商客服Agent中的程序级防幻觉流程。RAG首先召回候选商品ID但候选结果不会直接进入权威证据get_product_details和get_promotions使用SQLite按商品ID验证事实calculate_final_price重新读取单价和优惠并用Decimal执行确定性计算。EvidenceLedger记录已验证商品ID和工具结果中的合法数字。模型生成答案后validate_answer()检查商品事实是否有SQL证据、引用ID是否有效以及“元”“¥”“”金额是否出现在证据集合中。校验失败时返回SAFE_FALLBACK。这套机制能够拦截项目测试覆盖的无证据商品和虚假价格但仍不是万能的事实验证系统。下一篇将对比旧版ReAct与新版Function CallingRAG梳理整个项目的设计演进和适用场景。

相关新闻

双端优先队列原理与实现详解

双端优先队列原理与实现详解

1. 双端优先队列基础概念解析 双端优先队列(Double-Ended Priority Queue)是一种扩展了传统优先队列特性的数据结构,它允许高效地访问和删除队列中的最大和最小元素。这种数据结构在实时调度系统、网络流量管理和图形处理等领域有着广泛应用。 传统优先队列通常只支…

2026/11/14 5:24:15 阅读更多 →
Nginx代理HTTPS服务时如何忽略SSL证书验证

Nginx代理HTTPS服务时如何忽略SSL证书验证

1. Nginx代理HTTPS服务时忽略证书验证的典型场景在企业级网络架构中,Nginx作为反向代理服务器时经常需要处理HTTPS后端服务。当遇到以下情况时,管理员可能需要配置忽略SSL证书验证:测试环境使用自签名证书内部服务使用私有CA签发的证书证书链…

2026/11/8 2:26:31 阅读更多 →
自己怎么建设手机网站首页从零基础到上线的全流程实操指南

自己怎么建设手机网站首页从零基础到上线的全流程实操指南

现在是个移动互联网时代,谁要是还没个能在手机上流畅打开的网站,那基本上就跟丢了魂似的。我最近也在琢磨这个事,毕竟很多做小本生意或者个人品牌的朋友,都不愿意把命运完全交到那些昂贵的第三方平台手里。与其每个月交几百块的租赁费,还要看平台脸色限制功能,不如自己动…

2026/11/8 17:43:02 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/14 5:13:29 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/14 5:13:29 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/14 5:13:29 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/13 22:08:41 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/14 2:41:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/14 5:13:29 阅读更多 →

日新闻

周新闻

月新闻