构建 RAG + SQL 混合查询路由 Agent:基于 LlamaIndex、Milvus 与 Cleanlab Codex 的可信输出实践
【免费下载链接】ai-engineering-hubIn-depth tutorials on LLMs, RAGs and real-world AI agent applications.项目地址: https://gitcode.com/GitHub_Trending/ai/ai-engineering-hub
本篇技术指南基于仓库 rag-sql-router 的完整实现,讲解如何构建一个既能查询向量数据库(RAG 检索)、又能执行结构化 SQL 查询的自定义路由 Agent。文章将从环境搭建、工具封装、路由工作流到 Streamlit 交互应用逐层展开,并结合仓库源码揭示底层调用链,重点剖析 Cleanlab Codex 如何为 AI 输出提供自动响应验证与可信度评分,帮助读者掌握"Text2SQL + RAG"混合智能体从 0 到 1 的实战方案。
一、系统概览:为什么需要"路由"而不是"全都要"
传统方案往往让一个 Agent 同时绑定多个数据源,但不同类型的问题本质需要不同的查询路径:
- 结构化数据问题(如"休斯顿的人口是多少?")需要精确的 SQL 聚合与过滤,容错空间极小;
- 非结构化文档问题(如"这份报告里关于营收的结论是什么?")需要语义检索与片段合成,答案天然具有模糊性。
本项目的核心思想是把两类能力封装为两个独立工具——sql_tool与document_tool,再由 LLM 依据用户问题的语义自动选择(路由)调用哪一个,从而兼顾精确性与灵活性。仓库 README.md 明确指出:该系统引导你创建一个自定义 Agent,它可以查询Vector DB 索引(RAG 检索),也可以查询独立的 SQL 查询引擎。
与此同时,README 强调了整个系统最关键的组件——响应验证(Response Validation):"当所有人都在构建 Agent 时,没有人告诉你如何确保它们的输出是可靠的。" 这正是 Cleanlab Codex 在本项目中的角色:对每一次查询与回答进行自动校验,为输出提供可信度评分,并允许领域专家(SME)介入改进。
二、技术栈一览
根据 README.md 与 pyproject.toml,本项目的核心依赖如下:
| 组件 | 用途 | 仓库中的证据 |
|---|---|---|
LlamaIndex(llama-index>=0.12.52) | Agent 编排、Query Engine、Workflow 框架 | workflow.py |
Docling(llama-index-readers-docling) | 简化 PDF/DOCX/PPTX 文档解析 | tools.py |
Milvus + PyMilvus(pymilvus>=2.5.14) | 自托管向量数据库 | tools.py |
Cleanlab Codex(cleanlab-codex>=1.0.26) | 响应验证与可信度保障 | tools.py |
OpenRouter AI(llama-index-llms-openrouter) | 访问阿里 Qwen 模型 | app.py |
SQLAlchemy(sqlalchemy>=2.0.42) | SQLite 连接与查询 | tools.py |
Streamlit(streamlit>=1.47.1) | 交互式 Web 界面 | app.py |
HuggingFace Embeddings(llama-index-embeddings-huggingface) | 语义向量化(BAAI/bge-small-en-v1.5) | app.py |
项目要求 Python>=3.12。此外还依赖nest-asyncio(让 Streamlit 环境下可运行 asyncio)、pandas与plotly(数据库可视化)、torch(嵌入模型推理)。
三、环境搭建:三步跑通
3.1 启动 Milvus 向量数据库
Milvus 官方提供了 Docker 容器安装脚本。按 README.md 的操作,执行:
curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh bash standalone_embed.sh start脚本会以 Docker 方式拉起 Milvus standalone 实例,默认监听http://localhost:19530——这也是 tools.py 中setup_document_tool默认的milvus_uri参数值。如果 Milvus 未启动,文档向量化与索引写入将无法完成。
3.2 安装依赖
仓库使用 uv 管理依赖:
uv sync该命令会依据 pyproject.toml 与uv.lock锁定文件创建虚拟环境并安装全部依赖。
3.3 两条运行路径
- Jupyter Notebook 体验:运行 notebook.ipynb,该笔记本完整演示了路由(routing)、工具调用(tool calling)与响应验证(validating responses)三大主题;
- Streamlit 应用:执行以下命令启动交互界面:
streamlit run app.py然后在浏览器访问http://localhost:8501。
四、Cleanlab Codex:让 AI 输出"可信"的关键组件
README 将 Cleanlab Codex 定位为系统最关键的组件,并总结其五大价值:
- 自动检测:自动识别 AI 产生的不准确/无帮助响应;
- 持续改进:领域专家(SME)无需工程介入即可直接改进响应;
- 可信度评分:为每次响应提供可靠性指标;
- 实时验证:实时校验查询与响应;
- 分析:追踪改进率与响应质量随时间的变化。
4.1 在本系统中的工作流程
README 描述了四步闭环:
- 查询处理:你的查询由 Cleanlab Codex 自动验证;
- 响应验证:AI 响应被评分,衡量可靠性与准确性;
- SME 介入:领域专家可通过 Codex 界面改进响应;
- 持续学习:系统从已验证的响应中学习,服务后续查询。
4.2 源码中的验证调用链
从 tools.py 可以看到完整的落地实现。首先,create_codex_project(L24-L44)通过环境变量CODEX_API_KEY初始化客户端,创建命名项目并生成访问密钥;若缺少 API Key,则打印警告并优雅降级(Codex 验证被禁用,RAG 返回基础答案)。
document_query_tool(L191-L260)是验证的核心:
result = codex_project.validate( messages=messages, query=query, context=context_str, response=initial_response, )validate接收完整上下文(检索到的源片段拼接)、用户问题与初始回答,返回一个包含多维度评估结果的对象。随后代码执行最终响应选择逻辑:
final_response = ( result.expert_answer if result.expert_answer and result.escalated_to_sme else ( fallback_response if result.should_guardrail else initial_response ) ) trust_score = result.model_dump()["eval_scores"]["trustworthiness"]["score"]也就是说:若 Codex 判断需要升级给专家(escalated_to_sme)且专家已给出答案,则采用专家答案;若触发了护栏(should_guardrail),则返回兜底文案;否则保留原始 RAG 响应。可信度分数从eval_scores.trustworthiness.score中提取,与最终响应一并打包成字典返回,供上层工作流展示。
五、双工具封装:SQL 引擎与文档检索引擎的源码级解析
所有工具定义集中在 tools.py 中,并通过 LlamaIndex 的QueryEngineTool与FunctionTool统一暴露给 Agent。
5.1setup_sql_tool:自然语言 → SQL
def setup_sql_tool(db_path="city_database.sqlite", table_name="city_stats"): engine = create_engine(f"sqlite:///{db_path}") sql_database = SQLDatabase(engine) sql_query_engine = NLSQLTableQueryEngine( sql_database=sql_database, tables=[table_name], ) sql_tool = QueryEngineTool.from_defaults( query_engine=sql_query_engine, name="sql_tool", description=( "Useful for translating a natural language query into a SQL query over" " a table containing: city_stats, containing the population/state of" " each city located in the USA." ), ) return sql_tool要点:
- 数据源为仓库自带的 city_database.sqlite,其中
city_stats表包含city_name、population、state三列(前几条数据如 New York City / 8,336,000 / New York); NLSQLTableQueryEngine负责把自然语言翻译成 SQL 并执行;- 工具描述(description)是路由成败的关键:LLM 正是依据这段描述判断"人口/州"类问题应交给
sql_tool。
5.2setup_document_tool:Docling + Milvus + Codex 三合一
def setup_document_tool(file_dir, session_id=None, milvus_uri="http://localhost:19530"): reader, node_parser = DoclingReader(), MarkdownNodeParser() loader = SimpleDirectoryReader( input_dir=file_dir, file_extractor={".pdf": reader, ".docx": reader, ".pptx": reader, ".txt": reader}, ) docs = loader.load_data() unique_collection_id = uuid.uuid4().hex collection_name = f"rag_with_sql_{unique_collection_id}" vector_store = MilvusVectorStore(uri=milvus_uri, dim=384, overwrite=True, collection_name=collection_name) storage_context = StorageContext.from_defaults(vector_store=vector_store) vector_index = VectorStoreIndex.from_documents( docs, show_progress=True, transformations=[node_parser], storage_context=storage_context, )关键实现细节:
| 环节 | 实现 | 说明 |
|---|---|---|
| 文档解析 | DoclingReader | 统一处理 PDF/DOCX/PPTX/TXT 四种格式 |
| 节点切分 | MarkdownNodeParser | 按 Markdown 结构切分节点 |
| 向量存储 | MilvusVectorStore | 每个会话生成唯一 collection(rag_with_sql_<uuid>),overwrite=True,向量维度dim=384(与 bge-small-en-v1.5 输出维度一致) |
| 检索策略 | similarity_top_k=3 | 取最相关的 3 个片段 |
检索后使用自定义 QA 提示模板约束生成——要求模型"严格基于上下文作答、不引用先验知识、上下文不足时明确说明",随后进入上一节的 Codex 验证流程。最终通过FunctionTool.from_defaults封装为document_tool,其描述明确引导 LLM:"如果用户问题与 US 城市统计(人口和州)无关,请使用本文档检索工具。"
5.3 Codex 项目的会话级复用
get_or_create_codex_project(L54-L69)通过全局变量缓存 Codex 项目:同一会话内复用,新会话(不同session_id)才重新创建,避免频繁创建项目造成资源浪费。
六、RouterOutputAgentWorkflow:事件驱动的路由 Agent
路由编排位于 workflow.py,它继承 LlamaIndex 的Workflow基类,是一个基于事件(Event)与步骤(Step)的异步工作流。
6.1 事件与步骤拓扑
| 事件 | 作用 |
|---|---|
InputEvent | 输入事件,触发 LLM 决策 |
GatherToolsEvent | 携带 LLM 选中的工具调用列表 |
ToolCallEvent | 单个工具调用任务 |
ToolCallEventResult | 单个工具调用的结果消息 |
四个核心步骤构成循环:
prepare_chat:从StartEvent取出message,追加到聊天历史;chat:调用self.llm.achat_with_tools(self.tools, ...),把两个工具交给 LLM 决策。allow_parallel_tool_calls=True允许并行调用;若没有工具调用则直接返回StopEvent;dispatch_calls:把每个ToolSelection广播为独立的ToolCallEvent(通过ctx.send_event支持并发执行);call_tool:从tools_dict找到对应工具并执行await tool.acall(**tool_call.tool_kwargs)。特别地,当工具返回包含response与trust_score的字典时(文档工具),会将其拆解并写入ChatMessage.additional_kwargs,把可信度分数透传到上层 UI;gather:使用ctx.collect_events聚合所有工具结果,追加回聊天历史,然后重新触发InputEvent,让 Agent 基于工具结果进行下一轮推理——直到 LLM 认为无需再调用工具,输出最终答案。
从 workflow.py 的构造函数可见,工作流支持timeout(默认 10s,应用层传 120s)、disable_validation、verbose等参数,并可从Settings.llm兜底获取 LLM。
工作流的运行轨迹可通过draw_all_possible_flows可视化(见 notebook.ipynb),产物即为仓库中的 workflow_all_flows.html。
七、Streamlit 应用:从配置到交互的完整闭环
app.py 提供了完整的图形化界面,结构清晰可拆解为三部分。
7.1 侧边栏配置面板
用户需在侧边栏填入两个密钥(均为密码输入框):
- Codex API Key:写入
os.environ["CODEX_API_KEY"],用于响应验证; - OpenRouter API Key:用于初始化 LLM。
模型初始化逻辑(L372-L383)为:
llm = OpenRouter(model="qwen/qwen-turbo", api_key=_api_key) embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")随后赋值给全局Settings.llm与Settings.embed_model,供工作流与查询引擎使用。
7.2 文档上传与工作流组装
应用支持一次上传多个文档(pdf/docx/pptx/txt),保存到临时目录后调用setup_document_tool构建文档工具。工具组装遵循"SQL 优先"原则(L670-L686):
tools = [setup_sql_tool()] # ... 若已上传文档,则追加 document_tool之后用RouterOutputAgentWorkflow(tools=tools, verbose=False, timeout=120)初始化工作流,并缓存于st.session_state中;当密钥或文档变化时置workflow_needs_update标记以触发重建。
7.3 聊天界面与可信度可视化
process_query(L438-L520)通过asyncio.wait_for(..., timeout=60.0)包裹工作流运行,防止请求挂起。返回结果时,会从聊天历史中提取工具消息,判断走的是document_tool还是sql_tool,并展示信任度:
- ≥70%显示 🟢 绿色;
- ≥50%显示 🟡 黄色;
- <50%显示 🔴 红色。
信任度按round(trust_score * 100, 1)换算为百分比。聊天区还提供"Reset Chat"按钮,一键清空历史并重建工作流。
7.4 数据库可视化看板
点击"View Database"可展开完整的数据洞察面板(render_database_tab),包括:
- 指标卡:城市总数、总人口、州数量、平均人口;
- 图表:Top 10 人口柱状图、州分布环形图、人口散点图(Plotly 实现);
- Schema 展示:通过
PRAGMA table_info读取表结构; - 自定义 SQL:内置 6 条预置查询(如 "Top 10 cities by population"、"Cities with population > 1M"),也支持手写 SQL,结果可下载为 CSV。
八、运行验证:路由决策的实际表现
notebook.ipynb 内置了两个典型测试用例,展示了路由与验证的实际效果:
用例 1:结构化问题 → 路由到 sql_tool
Calling function sql_tool with msg {'input': 'What is the population of Houston, Texas?'} Chat message: The population of Houston, Texas is 2,303,000.与city_stats表中 Houston 的记录(2,303,000)完全吻合,验证了 Text2SQL 路径的精确性。
用例 2:非结构化问题 → 路由到 document_tool
Calling function document_tool with msg {'query': 'What is the weather in California?'} Chat message: The provided context does not have enough information to answer the question about the weather in California.由于文档上下文中没有天气信息,自定义 QA 模板的第三条规则触发——模型如实声明"上下文不足以回答",而不是编造答案。这正是响应验证与护栏机制的价值所在:宁可拒绝,也不幻觉。
九、扩展思考与最佳实践
从本仓库实现可以提炼出以下可复用的工程经验:
- 工具描述即路由策略:LLM 完全依赖
description决定调用哪个工具,务必用清晰、互斥的语言描述每个工具的适用边界(本项目中 SQL 工具明确限定于"美国城市人口/州",文档工具则声明"与城市统计无关的问题用我"); - 验证层与推理层解耦:RAG 检索 → Codex 验证 → 最终响应的三段式设计,让"生成"与"把关"分离,专家答案与护栏可以随时介入,无需改动检索链路;
- 优雅降级:
CODEX_API_KEY缺失时系统自动退回基础 RAG,Milvus 不可用时查询引擎同样会报错提示,保证任何环节故障都不至于静默产出低质量答案; - 会话隔离:Milvus collection 与 Codex project 均按会话(uuid)隔离,避免多用户数据串扰。
需要说明的是:本项目展示的是单表city_stats的 SQL 路由与通用文档 RAG 的组合;若需扩展到多表数据库或更多知识源,可在此基础上按同样的"工具封装 + 事件工作流"模式继续叠加,而验证层(Cleanlab Codex)保持不变。
十、结语
rag-sql-router提供了一个完整可运行的"Text2SQL + RAG"混合 Agent 参考实现:LlamaIndex 负责编排与检索,Docling 负责文档解析,Milvus 提供自托管向量存储,OpenRouter 接入 Qwen 模型,而 Cleanlab Codex 则补齐了多数教程缺失的一环——输出可靠性保障。无论你是要构建企业级文档问答,还是希望在现有 RAG 系统上叠加结构化查询能力,本仓库的 tools.py、workflow.py 与 app.py 都是值得直接参考的工程范本。
【免费下载链接】ai-engineering-hubIn-depth tutorials on LLMs, RAGs and real-world AI agent applications.项目地址: https://gitcode.com/GitHub_Trending/ai/ai-engineering-hub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考