Digital Librarian AI Agent 这个概念在 IBM 的相关技术分享中通常用来描述一类新的智能体它像图书馆管理员一样先理解用户问题再去不同数据源里查到资料最后整理成自然语言答案。落到工程实现上最容易被低估的难点是同一个 Agent 要同时访问 SQL 数据库和向量数据库。SQL 承担精确查询向量数据库承担语义检索LLM 负责路由与组织回答。这篇文章围绕这条主线展开先拆概念和架构再给出一个可用 Python 实现的最小闭环最后补上验证方法、排查路径和生产化建议。题目里的“Digital Librarian AI Agent”并不是一个需要复杂分布式系统的概念。它强调的是一套检索逻辑用户提出意图不明确的问题系统先判断应该走结构化查询还是走向量相似度检索或者两条链路都要走。真正动手做的时候你很快会遇到工具描述怎么写、函数参数怎么传、返回结果怎么合并、向量库里改完数据为什么查不到这些问题。下面这些内容就是在解决这些问题。1. 先理解 Digital Librarian AI Agent 的角色边界1.1 从图书馆管理员到数字图书管理员一个合格的图书馆管理员不会在读者问“有没有 2021 年出版的机器学习书”时先去翻一遍书架上的所有书。他会先查馆藏系统把年份、分类、库存这些精确字段过滤出来。如果读者换一种问法“我想找几本讲大模型训练但又不太难懂的书”这时管理员更多依赖对书籍内容的理解而不是图书编号。Digital Librarian AI Agent 就是把这种能力自动化。它至少要做三件事理解用户问题识别这是精确查询还是语义搜索。调用合适的检索工具比如 SQL 查询、向量检索、外部 API。把检索结果组织成自然语言答案必要时给出依据。这个定义和普通搜索的区别在于“主动编排”。普通搜索只有一个检索入口Agent 则要根据问题变化选择工具。工具选错了后面再强的模型也答不准。1.2 为什么一个 Agent 需要同时连接 SQL 和向量数据库很多人以为企业知识库只需要向量数据库。实际一旦做起来就会发现只用向量库远远不够。以图书系统为例常见的查询有这么几类“查一下《小王子》在哪个书库可否借阅。”这是结构化查询需要精确匹配书名、馆藏状态。“推荐几本讲成长焦虑的书。”这是语义查询需要理解“成长焦虑”和书籍内容之间的关系。“找一些 2020 年以后出版、适合初中生读的人工智能科普书。”这是混合查询先做语义筛选再做结构化过滤。如果只用 SQL语义模糊的问题基本答不上来因为数据库里没有“语义相似度”这个字段。如果只用向量数据库结构化过滤又很难做精确因为向量检索是近似匹配无法保证“2020 年以后”这个条件一定被满足。所以 Digital Librarian AI Agent 的架构里SQL 和向量数据库不是二选一而是互补。1.3 本文的落地主线这篇文章不打算停留在概念层面而是围绕“Digital Librarian AI Agent 连接 SQL 与向量数据库”做一个最小可运行案例。整体路径是用 SQLite 保存书目、借阅等结构化信息。用 Chroma 保存图书摘要与介绍提供语义检索。用 LLM 的函数调用能力让模型自动决定调用 SQL 工具还是向量检索工具。最后用一组验证问题检查 Agent 的检索行为。这个案例可以直接在本地运行适合作为后续企业知识库、资料问答系统、智能客服的基础原型。2. 架构设计SQL 与向量数据库怎么在一个 Agent 里共存2.1 两类数据在检索方式上的本质差异设计 Agent 之前先要把两类数据的特点分清。它们不是“换一种数据库存同一份内容”而是检索逻辑完全不同。对比项SQL 数据库向量数据库数据形态结构化表格字段固定文本块、文档、多媒体表示向量存储单位行记录向量 文本 metadata匹配方式等值、范围、模糊匹配余弦相似度、内积、欧氏距离查询结果精确、可重复近似、按相似度排序代表问题某个字段等于什么哪段内容语义上更接近典型场景订单、库存、用户资料、权限知识库问答、内容推荐、文档召回从这张表能看出Agent 必须根据问题类型选择检索入口。一个常见错误是把所有资料都塞进向量库结果用户问“库里有多少本书”时模型只能猜一个数字而不是执行COUNT(*)。因此准确答案是 SQL 的强项开放语义答案是向量检索的强项Agent 的价值就是做好这道选择题。2.2 查询路由Agent 如何决定走哪条链路在实现里路由策略主要有三种。第一种是规则路由。用关键词判断问题属于哪一类。比如出现“作者”“出版年份”“库存”就走 SQL出现“推荐”“类似”“关于”就走向量检索。优点是简单、逻辑可解释缺点是无法覆盖复杂表达。第二种是 LLM 函数调用路由。把 SQL 查询和向量检索都封装成工具给每个工具写清楚名称、能力描述、参数结构然后让 LLM 根据用户问题返回一个结构化调用。这是目前实践中最主流的做法因为它把“意图识别”和“参数抽取”都交给模型完成。第三种是混合决策。先用向量检索得到候选再用 SQL 字段过滤或者先用 SQL 缩小范围再对少量文本做向量重排序。这种链路适合复杂问题但工程复杂度也更高。最小实现阶段建议先做第二种因为代码结构清晰后续也容易扩展。2.3 最小技术选型与运行环境为了实现可复现的最小案例技术栈尽量简单组件选型说明语言Python 3.10生态成熟示例代码易读结构化存储SQLite单文件无需安装数据库服务适合本地学习向量数据库Chroma本地持久化便于查看和调试LLM 访问OpenAI 兼容接口通过环境变量传入 API Key依赖管理venv pip避免污染系统环境安装命令mkdir librarian-agent cd librarian-agent python -m venv .venv source .venv/bin/activate pip install chromadb openai这里要注意OpenAI 兼容接口并不只有一家很多模型服务都提供同样的chat/completions接口。只要代码里通过环境变量配置接口地址和密钥就能在不同模型之间切换。如果只是在学习环境跑通逻辑没有可用 API Key也可以先使用 3.4 节里给出的规则路由器做本地验证等拿到 Key 再切换到 LLM 路由。3. 最小实现用 Python 搭出 SQL 向量 LLM 的检索闭环3.1 初始化 SQLite 表结构先建立library.db用于保存图书精确信息。下面是一份简化表结构CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL, category TEXT, published_year INTEGER, library_id TEXT UNIQUE, status TEXT DEFAULT available ); CREATE TABLE borrow_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, reader_name TEXT, borrow_date TEXT );插入几条示例数据INSERT INTO books (title, author, category, published_year, library_id, status) VALUES (人工智能导论, 周明, 人工智能, 2019, LIB-AI-001, available), (机器学习基础, 王强, 机器学习, 2021, LIB-ML-002, borrowed), (Python 编程入门, 李娜, 编程, 2018, LIB-PY-003, available), (深入理解Transformer, 赵晨, 人工智能, 2023, LIB-NLP-004, available);表结构的设计目标是方便 SQL 工具按字段过滤。这里要特别注意工具传给 LLM 的字段必须和数据库字段一致。如果你在工具描述里说可以按“出版年份”过滤但函数内部用的是publish_year模型就会生成无法执行的参数。3.2 初始化 Chroma 向量库向量库保存的是图书摘要和简介。这里的核心不是把整本书存进去而是把“适合被语义检索的文本块”存进去。import chromadb from chromadb.utils import embedding_functions client chromadb.PersistentClient(path./librarian_db/chroma) collection client.get_or_create_collection( namebook_summary, metadata{hnsw:space: cosine} ) book_summaries [ { book_id: 1, title: 人工智能导论, summary: 系统讲解人工智能基本概念、搜索算法、知识表示适合初学者建立整体认知。 }, { book_id: 2, title: 机器学习基础, summary: 从线性模型讲到集成学习覆盖监督学习和无监督学习偏工程实践。 }, { book_id: 3, title: Python 编程入门, summary: 面向零基础读者的 Python 教程语法清晰配套大量练习。 }, { book_id: 4, title: 深入理解Transformer, summary: 以 Transformer 架构为主线讲解注意力机制、预训练模型和微调方法。 } ]写入向量库时需要同时提供documents、metadatas和idscollection.add( documents[item[summary] for item in book_summaries], metadatas[ {book_id: item[book_id], title: item[title]} for item in book_summaries ], ids[fbook_{item[book_id]} for item in book_summaries] )写入后摘要文本会被自动向量化。不同版本的 Chroma 默认使用的 embedding 实现可能不同首次运行可能会下载或缓存模型文件因此学习环境尽量固定版本避免换一台机器后检索结果出现差异。3.3 封装两个检索工具SQL 工具负责精确查询。下面这个函数会根据传入参数动态拼接条件但值全部使用参数占位符import sqlite3 DB_PATH ./librarian_db/library.db def query_books_by_sql(params: dict) - list[dict]: conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row where [] values [] if params.get(author): where.append(author ?) values.append(params[author]) if params.get(category): where.append(category ?) values.append(params[category]) if params.get(published_year): where.append(published_year ?) values.append(params[published_year]) if params.get(status): where.append(status ?) values.append(params[status]) sql SELECT * FROM books if where: sql WHERE AND .join(where) sql LIMIT 20 rows conn.execute(sql, values).fetchall() conn.close() return [dict(row) for row in rows]这里为什么要用?占位符而不是直接把params[author]拼进 SQL因为用户输入本质上是不可信字符串。如果直接把输入拼进 SQL输入内容会被当作 SQL 语句的一部分执行这是非常危险的注入风险。使用参数化查询后输入只会被当成普通值处理。向量检索工具负责语义搜索def search_similar_books(params: dict) - list[dict]: question params[question] top_k int(params.get(top_k, 3)) results collection.query( query_texts[question], n_resultstop_k, include[documents, metadatas, distances] ) if not results[ids] or len(results[ids][0]) 0: return [] formatted [] for i in range(len(results[ids][0])): formatted.append({ book_id: results[metadatas][0][i][book_id], title: results[metadatas][0][i].get(title, ), similarity: 1 - results[distances][0][i], matched_text: results[documents][0][i] }) return formatted向量检索返回的是相似度最高的前top_k条记录不是全量结果。这也是它和 SQL 的显著差异。你不能把用户的自然语言问题直接变成一条精确 SQL但可以把它变成向量然后计算距离。3.4 用 LLM 函数调用实现自动路由把两个工具封装成工具描述交给 LLM 选择。工具描述是决定路由是否准确的关键描述里要写清楚“什么情况下用这个工具”。import json from openai import OpenAI llm_client OpenAI() tools [ { type: function, function: { name: query_books_by_sql, description: 用于精确查询图书信息支持按作者、分类、出版年份、借阅状态过滤。适合用户提到明确字段或条件的场景。, parameters: { type: object, properties: { author: {type: string, description: 作者名称}, category: {type: string, description: 图书分类}, published_year: {type: integer, description: 出版年份}, status: {type: string, description: 馆藏状态available 或 borrowed} }, required: [] } } }, { type: function, function: { name: search_similar_books, description: 用于按语义查找图书适合用户描述某个主题、内容或阅读感受无法用精确字段表达需求的场景。, parameters: { type: object, properties: { question: {type: string, description: 用户问题或改写后的语义搜索词}, top_k: {type: integer, description: 返回数量默认 3} }, required: [question] } } } ] TOOL_REGISTRY { query_books_by_sql: query_books_by_sql, search_similar_books: search_similar_books }然后是 Agent 主循环。核心思路是把用户问题发给 LLM如果模型返回tool_calls就执行对应工具把工具结果作为新的消息再发给模型直到模型返回最终回答。def run_agent(user_question: str, max_iterations: int 3) - str: messages [ {role: system, content: 你是数字图书管理员根据用户问题选择合适的工具检索图书信息再用中文简洁回答。}, {role: user, content: user_question} ] for _ in range(max_iterations): response llm_client.chat.completions.create( modelMODEL_NAME, messagesmessages, toolstools, tool_choiceauto ) message response.choices[0].message if not message.tool_calls: return message.content or 没有生成回答。 messages.append(message) for tool_call in message.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) print(f[Agent] 调用工具: {fn_name}, 参数: {fn_args}) result TOOL_REGISTRY[fn_name](fn_args) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) return 已达到最大工具调用轮数未生成最终回答。如果没有 API Key又希望先验证检索逻辑可以先用规则路由代替 LLM 路由def rule_router(question: str) - str: sql_keywords [作者, 分类, 出版年份, 库存, 状态, ISBN] vector_keywords [推荐, 类似, 关于, 内容, 入门, 通俗] if any(word in question for word in sql_keywords): return query_books_by_sql if any(word in question for word in vector_keywords): return search_similar_books return clarify规则路由只能作为调试手段。它的优点是稳定、没有额外成本缺点是面对同义表达时很容易选错。生产环境建议优先使用函数调用同时保留一个兜底策略。4. 运行验证用 5 类问题检验 Agent 是否选对了工具4.1 构造覆盖不同场景的验证问题Agent 做完后不能只看它能不能回答还要看它有没有选对工具。下面这组问题适合作为回归测试集问题预期路由验证重点有哪些 2023 年出版的人工智能书SQL结构化过滤是否准确想找一本讲注意力机制的书内容偏工程。向量检索语义匹配是否命中 Transformer 那本图书馆里有周明写的书吗SQL作者等值查询是否正确推荐几本适合零基础读者的编程书。向量检索是否理解“零基础”和“入门”是同一语义深度学习和机器学习有什么入门书可能先向量再 SQL多步调用是否能完成实际运行后你会在控制台看到类似输出[Agent] 调用工具: query_books_by_sql, 参数: {published_year: 2023, category: 人工智能} [Agent] 工具返回: [{id: 4, title: 深入理解Transformer, author: 赵晨, published_year: 2023}] [Agent] 最终回答: 2023 年出版的人工智能类图书有《深入理解Transformer》作者是赵晨。如果看到工具选择不符合预期不要急着调 prompt。先确认工具描述里的字段和参数名是否足够清晰再看模型是不是在多个相似工具之间产生了混淆。4.2 验证 SQL 查询与向量检索的边界验证时要刻意测试两条链路的边界。SQL 链路的边界是“条件表达不明确”。比如“有没有适合新手的 Java 书”这个问题里“新手”不是表字段规则路由和模型都可能不知道该不该走 SQL。此时应该走向量检索。向量链路的边界是“精确值查询”。比如“库存还有几本书”这类问题没有语义模糊空间走向量检索会得到一段文本而不是数字。此时应该走 SQL。如果 Agent 反复选错可以检查工具描述是否出现了互相覆盖的句子。工具描述越短越直接模型越容易选择。4.3 用评测集衡量效果而不是只看单个回答单次提问回答正确说明不了 Agent 稳定。建议准备一份包含 20 到 50 条问题的评测集每题标注预期工具、预期参数和预期答案要点。每次改动工具描述或数据库结构后重新跑一遍评测集观察工具选择准确率。参数抽取正确率。最终回答命中率。无结果时的拒答率。评测集不需要一开始就很完善从 10 条问题起步也可以关键是持续积累。这样后续从 Chroma 换成 Milvus或者从 SQLite 迁移到 PostgreSQL 时能快速发现回归问题。5. 常见问题排查查不到、答非所问和安全风险5.1 先建立排查主线Agent 问题往往不是单一原因造成的。遇到“查不到”或“答错”不要直接改 prompt按下面顺序排查用户输入本身是否清楚。数据是否真的存在。Agent 是否正确调用了工具。工具参数是否正确。检索结果是否为空。LLM 是否正确组织最终回答。是否存在超时、截断或格式解析问题。这个顺序的价值在于它能帮你把“数据问题”和“模型问题”分开。如果工具调用日志里根本没有执行 SQL那说明问题出在路由而不是 SQL 语句本身。5.2 向量库添加数据后查询不到这是最常见的坑之一。表现是数据明明写入成功但自然语言提问时返回空结果。问题现象可能原因检查方式处理建议向量库返回空写入和查询使用了不同的集合名打印collection.name统一使用get_or_create_collection的 name向量库返回空写入和查询使用不同 embedding 函数检查初始化参数写入端和查询端必须共用同一套 embedding 配置查询结果与预期不符数据写入后没有持久化确认 PersistentClient 路径不要使用内存模式后重启进程查询结果偏少top_k 设置过小查看工具参数适当调大 top_k比如 5 到 10重复运行后数据翻倍每次启动都 add没有去重检查日志中 ids写入前先查询旧 ids或使用upsert这里也要提醒一下Chroma 本地持久化会生成一系列文件不要手工去改里面的内容。备份时直接备份整个数据目录即可。如果你确实想观察内部结构可以用 SQLite 查看器打开 Chroma 生成的数据文件但不同版本的内部表和字段不完全一致页面含义以你安装版本的源码为准。5.3 LLM 选错工具或参数抽取错误如果 Agent 把“有哪些 2023 年的书”路由到了向量检索或者调用 SQL 时没有传published_year问题一般出在三点工具描述太长模型抓不住关键信息。工具描述太短没有说明适用场景。参数定义和函数实现不一致比如描述里写“年份”参数名却是year。解决方式是在工具描述里增加典型问题和反例。比如{ name: query_books_by_sql, description: 当用户需要按作者、分类、出版年份、借阅状态等精确条件查询时使用。如果用户表达的是模糊主题请使用 search_similar_books。, }还要检查模型版本是否支持tools参数。老版本模型可能只支持文本补全不支持函数调用这种情况下需要升级模型或改用文本路由。5.4 最终回答出现幻觉或引用不准确Agent 答错不一定是检索失败也可能是检索成功但模型错误合并了信息。比如 SQL 返回了作者“周明”模型最终回答成“王强”这就不是数据问题而是生成阶段缺乏约束。常用的做法是在 system prompt 里明确要求只能基于工具返回结果回答不要补充数据库中不存在的信息。还可以要求模型在回答后附上来源比如“根据《人工智能导论》馆藏记录”。更强的约束是输出校验。对关键字段比如书名、作者、年份在最终回答生成后做一次字符串匹配校验不一致就重新生成或拒答。5.5 SQL 注入与提示词注入风险这部分必须单独讲。把 SQL 工具暴露给 LLM本质上是让模型可以根据用户输入生成 SQL 参数。如果实现里直接拼接字符串用户输入里的恶意内容就会进入 SQL 结构造成注入风险。防护手段不是教模型“识别恶意输入”而是从架构上限制所有 SQL 值必须使用参数化查询。工具内部只允许执行预先写好的查询模板不要执行任意 SQL 文本。不要让 LLM 直接传 SQL 字符串而是传参数字典由函数内部拼装白名单条件。工具账号或数据库连接只授予最小权限例如只读权限。涉及删除、更新、导出等敏感操作不要暴露给 Agent 工具。同时还要关注提示词注入。向量库里存储的文本可能来自外部文档这些文本可能包含“忽略之前指令”之类的句子。不要把检索到的内容无脑当作系统指令要把它们当作待处理数据并在 system prompt 里说明外部内容不可信。6. 生产化实践从最小原型到企业知识库要补哪些工作6.1 学习环境与生产环境的差异最小实现能跑通不等于可以直接上生产。两者的差异主要集中在数据规模、并发、权限和可观测性上。维度学习环境生产环境SQL 存储SQLite 单文件PostgreSQL、MySQL 等独立数据库向量存储Chroma 本地目录Milvus、pgvector、Elasticsearch 等独立集群Embedding默认模型或 API按数据域微调模型或固定商用 embedding APILLM 路由OpenAI 兼容接口增加限流、超时、重试、熔断工具权限本地可直接执行按租户、部门、数据域做隔离日志控制台 print结构化日志、链路追踪、指标监控数据更新手动写入增量导入、定时同步、删除策略不要等到生产环境再考虑权限。最小实现阶段就要把“工具最小权限”这个意识建立起来。SQLite 本地文件虽然没有复杂的账号体系但代码里如果早早养成参数化查询和参数白名单的习惯后面换到 PostgreSQL 时不会踩太多坑。6.2 可复用检查清单每次修改 Agent 或数据库结构后建议按这份清单检查工具名称、参数名和函数签名是否一致。用户输入是否全部经过参数化处理。SQL 工具是否限制返回行数防止全表扫描。向量库写入端和查询端的 embedding 是否一致。SQL 表字段和向量 metadata 是否能关联上。Agent 日志是否完整记录工具调用、参数、返回条数和耗时。无结果时是否有兜底回答而不至于生成幻觉。外部文档内容是否被当作不可信数据处理。数据删除后向量库中的旧向量是否同步清理。评测集是否已经覆盖精确查询、语义查询和混合查询三类。这份清单不是一次性工作。每加一个新工具、每改一个字段类型都要重新检查一遍。6.3 扩展方向多工具、多轮对话和可观测性完成最小闭环后可以沿着三个方向扩展。第一个方向是工具扩展。除了 SQL 和向量检索还可以接入外部图书接口、库存系统、预约系统把 Agent 从“只读检索”升级为“可执行操作”。这时要格外注意操作类工具的权限控制比如借书、预约属于写操作必须增加二次确认和审计日志。第二个方向是对话能力。当前实现每次只处理一个问题没有会话记忆。实际使用中用户会追问“那这本有电子版吗”Agent 需要理解“这本”指的是上一轮的书。可以引入对话历史把最近的几轮消息传给 LLM让路由在上下文基础上决策。第三个方向是可观测性。生产级 Agent 必须能看到每一次工具选择、参数内容、检索耗时和 token 消耗。推荐把工具调用日志输出成结构化 JSON记录到日志系统方便回放和分析。否则用户反馈“答错了”你很难知道是路由选错、参数抽错还是检索结果本身就没有匹配项。回到最开始的问题Digital Librarian AI Agent 的工程重点不在于“调用了一个多聪明的模型”而在于能否把结构化查询和语义检索正确编排起来。先用最小实现跑通这条路再逐步补齐权限、监控、评测和数据治理才是一个可以长期维护的知识库问答系统该有的样子。对新手来说最有价值的练习不是追求复杂框架而是把 SQLite、Chroma 和 LLM 函数调用这三层关系理清楚然后不断扩充评测集观察 Agent 在边界场景下的真实表现。