ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">在我们之前的文章中我们已经了解了两种预检索的优化策略。比如怎么“翻译”用户那七零八落的问题,还有怎么做一个聪明的“导航员”,把问题带到正确的数据源。ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">那么,现在问题来了,我们知道去哪里找数据了,那我们如何查不同类型的数据呢?ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">这就是我们今天想聊的另一种预检索优化策略:ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: inherit;color: rgb(0, 152, 116);">查询构建(Query Construction)。ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">在本文中,我们将聚焦于如何将用户的自然语言问题,翻译成数据源能理解的那套"行话"。比如SQL语言,或者Cypher查询语句。ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;display: table;padding: 0.3em 1em;color: rgb(255, 255, 255);background: rgb(0, 152, 116);border-radius: 8px;box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px;">什么是查询构建?ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">传统那套RAG,把问题变成一串向量数字,再去另一堆向量数字里找最相似的。这招在处理非结构化的文字,比如博客、文章时,确实管用。我得承认,这很巧妙。ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">但世界不是只有文字啊。ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">要是我们面对的是一张冷冰冰的Excel表,或是一座结构严密、壁垒分明的关系型数据库呢?我第一次遇到这个问题时,真的有点懵。你总不能跟一张员工薪资表谈“语义相似”吧?它有它的脾气,有行有列,有不容置疑的数据类型。跟它打交道,你得说它的“方言”——那些结构化的查询语言。ingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;letter-spacing: 0.1em;color: rgb(63, 63, 63);">这感觉,就像从一个诗人,突然要转变成一个会计。查询构建技术流派1. Text-to-SQL -- 从自然语言到SQL转换企业里最常见的就是这种“会计式”的数据了。MySQL、PostgreSQL... 它们就像一个个巨大的账本,记录着一切。 一个真正好用的RAG系统,我觉得它不应该偏科,不能只会读诗(非结构化文档),也得会算账(结构化数据)。 如果能让系统自己学会把我们的日常白话,转换成SQL,那该多好。用户只管提问,系统默默地把查询、整合、生成答案这些脏活累活全干了。这听起来才够智能,不是吗? 要做到这一点,其实没那么玄乎。关键就两步: - 1.数据库描述:你得先让大模型看明白数据库长什么样,有哪些表,表里有哪些字段。
- 2.少量示例:在提示词里给它几个标准提问的例子,它就能有样学样。
用一个简单的实现流程代码来理解: - 1. 创建一个
SQLite数据库,并创建一个包含一些示例数据的sales_database表。importsqlite3
def__init__(self, db_path="sales_database.db"): """ 初始化转换器 Args: db_path (str): SQLite数据库文件路径 """ self.db_path = db_path self.conn =None self.cursor =None
defconnect_database(self): """连接数据库""" try: self.conn = sqlite3.connect(self.db_path) self.cursor =self.conn.cursor() print(f"✅ 成功连接数据库:{self.db_path}") exceptExceptionase: print(f"❌ 数据库连接失败:{e}") raise
defcreate_sample_database(self): """创建示例数据库和数据""" try: # 创建销售数据表 self.cursor.execute(''' CREATE TABLE IF NOT EXISTS sales_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_name TEXT NOT NULL, quantity INTEGER NOT NULL, sale_date TEXT NOT NULL, revenue REAL NOT NULL, region TEXT NOT NULL, customer_type TEXT NOT NULL ) ''')
# 清空现有数据 self.cursor.execute('DELETE FROM sales_data')
# 插入示例数据 sample_data = [ ('iPhone 15',10,'2023-07-15',15000.0,'北京','个人'), ('iPhone 15',5,'2023-08-10',7500.0,'上海','个人'), ('MacBook Pro',20,'2023-07-20',40000.0,'广州','企业'), ('iPhone 15',15,'2023-09-01',22500.0,'深圳','个人'), ('iPad Air',7,'2023-09-15',3500.0,'北京','个人'), ('MacBook Air',12,'2023-08-25',12000.0,'上海','企业'), ('iPhone 14',8,'2023-07-30',8000.0,'成都','个人'), ('iPad Pro',6,'2023-09-10',7200.0,'杭州','企业'), ('MacBook Pro',3,'2023-08-05',6000.0,'武汉','个人'), ('iPhone 15 Pro',25,'2023-09-20',37500.0,'北京','企业'), ]
self.cursor.executemany(''' INSERT INTO sales_data (product_name, quantity, sale_date, revenue, region, customer_type) VALUES (?, ?, ?, ?, ?, ?) ''', sample_data)
self.conn.commit() print("✅ 示例数据库和数据创建成功")
# 显示数据统计 self.cursor.execute('SELECT COUNT(*) FROM sales_data') count =self.cursor.fetchone()[0] print(f"📊 数据库中共有{count}条销售记录")
exceptExceptionase: print(f"❌ 数据库创建失败:{e}") raise
- 2. 使用LLM生成查询
importos fromdotenvimportload_dotenv fromlangchain_DeepSeekimportChatDeepSeek fromlangchain_core.promptsimportChatPromptTemplate
defgenerate_sql(self, natural_query): """ 使用DeepSeek模型生成SQL查询 将所有SQL生成相关的初始化和配置集中在此函数中,便于理解整个生成流程 Args: natural_query (str): 自然语言查询 Returns: str: 生成的SQL语句 """ try: print("🔧 步骤1: 初始化DeepSeek模型...") # 初始化DeepSeek模型 llm = ChatDeepSeek( model="deepseek-chat", temperature=0.1, # 设置较低温度确保SQL生成的准确性 max_tokens=1024, api_key=os.getenv("DEEPSEEK_API_KEY"), )
print("📋 步骤2: 准备数据库Schema信息...") # 数据库schema信息 schema_info =""" 数据库Schema信息:
表名: sales_data 字段说明: - id: INTEGER PRIMARY KEY (主键,自增) - product_name: TEXT (产品名称) - quantity: INTEGER (销售数量) - sale_date: TEXT (销售日期,格式:YYYY-MM-DD) - revenue: REAL (销售收入) - region: TEXT (销售区域) - customer_type: TEXT (客户类型:个人/企业) """
print("📝 步骤3: 创建SQL生成提示模板...") # 创建SQL生成提示模板 sql_prompt = ChatPromptTemplate.from_template( """ 你是一个专业的SQL查询生成专家。基于以下数据库schema,将用户的自然语言查询转换为准确的SQL语句。 {schema} 规则要求: 1. 只生成SQL语句,不要包含任何解释文字 2. 确保SQL语法正确且符合SQLite标准 3. 使用适当的聚合函数、WHERE条件、GROUP BY、ORDER BY等 4. 日期比较请使用字符串比较(如:sale_date >= '2023-07-01') 5. 产品名称匹配请使用LIKE操作符支持模糊查询 6. 如果查询涉及时间范围,请合理解释季度、月份等时间概念 用户查询: {query} SQL语句:""" )
print("⚙️ 步骤4: 格式化提示词...") # 格式化提示词 formatted_prompt = sql_prompt.format( schema=schema_info, query=natural_query )
print("🤖 步骤5: 调用DeepSeek模型生成SQL...") # 调用DeepSeek模型生成SQL response = llm.invoke(formatted_prompt) sql_query = response.content.strip()
print("✨ 步骤6: 清理和格式化SQL语句...") # 清理SQL语句(移除可能的markdown格式) ifsql_query.startswith("```sql"): sql_query = sql_query.replace("```sql","").replace("```","").strip() elifsql_query.startswith("```"): sql_query = sql_query.replace("```","").strip()
print("✅ SQL生成完成!") returnsql_query
exceptExceptionase: print(f"❌ SQL生成失败:{e}") returnNone
- 3. 执行生成的 SQL 查询
defexecute_sql(self, sql_query): """ 执行SQL查询
Args: sql_query (str): SQL查询语句
Returns: list: 查询结果 """ try: self.cursor.execute(sql_query) results =self.cursor.fetchall()
# 获取列名 column_names = [description[0]fordescriptioninself.cursor.description]
returnresults, column_names
exceptExceptionase: print(f"❌ SQL执行失败:{e}") returnNone,None
- 4. 示例输出
📝 用户查询: 2023年第三季度iPhone 15的总销售收入是多少? -------------------------------------------------------------------------------- 🔄 正在生成SQL查询... 🔧 步骤1: 初始化DeepSeek模型... 📋 步骤2: 准备数据库Schema信息... 📝 步骤3: 创建SQL生成提示模板... ⚙️ 步骤4: 格式化提示词... 🤖 步骤5: 调用DeepSeek模型生成SQL... ✨ 步骤6: 清理和格式化SQL语句... ✅ SQL生成完成! 🔍 生成的SQL: SELECT SUM(revenue) AS total_revenue FROM sales_data WHERE product_name LIKE '%iPhone 15%' AND sale_date >= '2023-07-01' AND sale_date <= '2023-09-30'; -------------------------------------------------------------------------------- ⚡ 正在执行查询... 📊 查询结果: total_revenue --------------- 82500.0
2. Text-to-Cypher -- 从自然语言到图数据库查询图数据库是一种基于图结构进行数据存储的形式。这东西跟关系型数据库给人的感觉完全不一样。如果说SQL是精准、严谨的法律条文,那Cypher(图数据库的查询语言)就像是在描绘一张复杂的人际关系网。 它不关心一行行的数据,它关心的是实体(节点)和它们之间千丝万缕的联系(关系)。 - • “糖尿病”会有“头痛”这个症状,这个“会有”就是一条关系。
这种结构,简直是为了回答那些“打破砂锅问到底”的复杂问题而生的。 比如,你问:“什么药能治那些既会引起头痛、又会引起发烧的病?” 传统的RAG可能会被问傻,因为它很难找到一篇文章正好把这几样东西都凑在一起说。但对图数据库来说,这太自然了。它会沿着“关系”的藤蔓,一步步帮你把答案“摸”出来。 用它的语言Cypher写出来,大概是这个感觉: // 寻找同时拥有'头痛'和'发烧'两种症状的疾病 MATCH (s1:Symptom {name: '头痛'}), (s2:Symptom {name: '发烧'}) MATCH (disease isease)-[:HAS_SYMPTOM]->(s1) MATCH (disease)-[:HAS_SYMPTOM]->(s2)
// 寻找能够治疗这些疾病的药物 MATCH (drug rug)-[:TREATS]->(disease)
// 返回药物和对应治疗的疾病名称 RETURN drug.name, disease.name
你看,它思考的方式,是不是更像人脑的联想?把自然语言翻译成Cypher的过程,和翻译成SQL大同小异,都是让大模型去理解、匹配、然后生成。 Text-to-Cypher的实现主要流程是:
- •自然语言理解:你先得让它听懂你的“人话”,明白你到底想干嘛。
- •Schema匹配:你给它一张家族图谱(数据库的schema),让它把你话里提到的人和事在图谱上对号入座。
- •Cypher生成:等它都搞清楚了,它就会学着用图谱的“官方语言”,写出一句像模像样的查询。
- •查询验证与执行:它还会自己检查一下语法,确认无误后才把问题真正递出去。
由于篇幅原因,详细的可运行示例代码,请访问我的GitHub仓库,仓库链接见文章底部。
3. Self-Query Retriever -- 通过自然语言生成元数据过滤器在向量数据库这边,我们虽然可以直接用自然语言去查,但也不是没有优化的空间。在该环节中也有一些预检索优化技巧,Self-Query Retriever就是其中一种。 它的想法是:为什么不让用户的提问“自己告诉自己”该怎么查? 它会自动从你的问题里,抠出一些关键词作为“元数据”过滤条件。比如你问“给我找找2023年关于AI的论文”,它会先用“2023”和“AI”这两个硬性条件,把海量的文档筛掉一大批,再在剩下的小范围里去做向量相似度计算。 这不就是我们人脑处理信息的方式吗?先分类,再细看。简单,但极其有效。 同样,这个的完整代码我也放在GitHub上了,有兴趣可以去看看,仓库链接见文章底部。
总结从SQL的严谨,到Cypher的关联,再到Self-Query的小巧思,我们其实一直在做一件事:努力让机器放下身段,来迁就我们的思考方式。 这不只是个“翻译”工作,我觉得这更像是在驯兽,或者说,是在和一个异类寻找沟通的桥梁。我们希望它能突破那些冷冰冰的数据结构,真正理解我们字里行间那些模糊、跳跃、甚至充满情感的意图。 这个过程还在继续,远没到画上句号的时候。或许,下一代智能应用,真的能像个老朋友一样,听懂我们所有的言外之意吧。谁知道呢。 |