
1. 从一条文字工单说起为什么选大模型加查询工具这条路线文字工单这东西做过运维、客服、售后或者内部 IT 支持的人都不陌生。用户提交一段话比如“我上周买的打印机今天开机一直闪红灯订单号是 20250312-8871麻烦帮我查下能不能换货”这段文字里混着故障描述、订单信息、诉求意图甚至还有情绪。传统做法是人工读一遍判断类型再去后台系统里翻订单、查物流、看售后政策最后回复。一个人一天处理两三百条就到头了而且状态好坏直接影响判断质量。我这次要聊的就是怎么用大模型配合一个查询工具把这类文字工单的处理流程先跑起来。注意我的用词是“先跑起来”不是“一步到位做成生产级系统”。很多人在搭建 AI Agent 的时候容易犯一个毛病一上来就想把意图识别、多轮对话、知识库、工单流转、自动回复全部做完结果卡在环境配置和接口调试上两周过去连一个能演示的闭环都没有。我的建议一直是反过来的——先用最小可运行单元把主链路打通再逐步加东西。这条最小链路的核心就是两个东西一个大模型负责“读懂人话并决定做什么”一个查询工具负责“真的去数据里把结果捞出来”。大模型本身不知道你的订单表长什么样也不知道库存系统里有没有货它擅长的是理解自然语言、做推理、生成结构化调用参数。查询工具则是它的手和脚负责执行具体的数据检索。两者通过 Tool Calling 机制连接起来就形成了一个能处理文字工单的 AI Agent 雏形。适合谁来参考这篇内容如果你是会写一点 Python、懂基本的 API 调用、想从 0 到 1 搭建一个 AI Agent 练手项目的开发者这篇就是写给你的。如果你是大模型学习路线上的新手已经看过提示词工程和上下文工程的基础内容但还没真正动手接过工具那这篇也合适。甚至你只是想搞清楚“AI Agent 到底是怎么跑起来的”跟着走一遍也能有直观感受。我不假设你有微调经验也不要求你本地部署大模型用免费的 API 加上一个 SQLite 查询工具就能起步。2. 整体设计思路为什么是“大模型 查询工具”而不是别的组合2.1 文字工单处理的核心难点拆解先把这个问题的难点说清楚后面选型才有依据。文字工单处理看起来简单实际上至少包含四层任务。第一层是意图识别用户到底是要查订单、要退款、要报修还是单纯发泄情绪。第二层是信息抽取从一段自由文本里把订单号、商品名、时间、故障现象这些关键字段抠出来。第三层是数据查询拿着抽出来的字段去对应的数据源里检索。第四层是结果组织把查到的原始数据翻译成用户能看懂的话。传统方案里这四层要么用规则引擎硬编码要么用专门的 NLP 模型分别训练。规则引擎的问题是写不完用户换个说法就匹配不上专门训练模型的问题是成本高每个业务域都要标注数据、训练、调参。大模型出现之后前两层和第四层它天然就能做因为它就是在海量文本上训练出来的理解意图和生成回复是它的强项。唯独第三层它做不了因为数据在你的数据库里不在它的参数里。所以整个设计的核心判断就是把大模型擅长的事交给大模型把大模型做不了的事交给工具。这就是 Tool Calling 存在的意义。大模型不直接查数据库它输出一个结构化的调用请求比如“我要调用 query_order 这个工具参数是 order_id20250312-8871”然后由外部程序真正执行查询再把结果喂回给大模型让它组织成最终回复。2.2 为什么选查询工具作为第一个接入的能力有人会问为什么第一个接入的是查询工具而不是写入工具、通知工具或者别的。我的考虑有三点。第一查询是只读操作风险最低。你让大模型去执行写操作万一参数抽错了可能把别人的订单改了这种事故在练手阶段完全没必要冒。第二查询的输入输出边界清晰容易验证。给一个订单号返回一条记录对不对一眼就能看出来调试成本低。第三查询是工单处理里最高频的动作。大部分工单的本质就是“帮我查一下”把查询跑通了这个 Agent 就已经有实用价值了。从 AI Agent 开发的角度看查询工具也是理解 Tool Calling 最好的切入点。它足够简单简单到你能把整个调用链路看得清清楚楚又足够典型典型到你换成别的工具时套路完全一样。我见过不少人一上来就搞多工具编排、多智能体协作结果连单个工具的调用参数怎么传都没搞明白。先把一个查询工具吃透后面加十个工具都是复制粘贴的事。2.3 大模型选型的实际考量关于大模型选择我不打算给一个绝对答案因为这东西变化太快而且每个人的约束条件不一样。但我可以给你一套判断逻辑。如果你只是想练手、验证流程优先选有免费额度或者免费 API 的模型别一上来就充钱。如果你对数据隐私敏感考虑本地部署但要有心理准备本地跑 7B 级别的模型工具调用的稳定性会比云端大模型差一些需要更多提示词上的调教。具体到工具调用能力这是选型的硬指标。不是所有大模型都支持 Tool Calling有些模型虽然能对话但你让它输出结构化的函数调用格式它就开始胡说。选之前一定要确认两件事第一官方文档里明确写了支持 function calling 或者 tool use第二社区里有实际跑通的案例。我个人的经验是参数量在 7B 以上的指令微调模型配合清晰的工具定义基本都能跑通简单的单工具调用。如果你用的是更小的模型或者没经过指令微调的基座模型那就要做好反复调试的准备。还有一个容易被忽略的点是上下文长度。文字工单本身不长但如果你要把历史工单、知识库片段、工具返回结果都塞进上下文长度就上去了。起步阶段不用太纠结8K 上下文足够跑通流程等真正要处理复杂工单时再考虑更长的上下文或者做上下文工程优化。2.4 查询工具的技术选型查询工具这块我用的是 SQLite。原因很直接零配置、单文件、Python 标准库自带。你不需要装 MySQL、不需要配用户权限、不需要起服务一个 .db 文件就是整个数据库。对于练手项目来说这是最低摩擦的选择。等你把流程跑通了换成 MySQL 或者别的数据库无非是改一下连接字符串和 SQL 方言核心逻辑不变。有人可能会问热词里提到“mysql 查询工具 免费”是不是应该用 MySQL。我的看法是如果你本身就在用 MySQL那直接用没问题。但如果你是从零开始搭练手项目为了一个查询功能去装一整套数据库服务性价比不高。SQLite 能让你把注意力放在 Agent 逻辑上而不是环境配置上。等你需要多用户并发、需要远程访问的时候再迁移也不迟。工具的定义方式我用的是最朴素的 Python 函数加 JSON Schema 描述。没有用 LangChain 之类的框架也没有用 Spring AI 那套。不是说框架不好而是起步阶段我希望每一行代码都是透明的出了问题我能立刻定位。框架帮你省了样板代码但也藏了细节等你需要定制的时候反而更麻烦。先把裸的调用链路写一遍之后再用框架就是降维打击。3. 核心细节解析Tool Calling 到底是怎么跑起来的3.1 大模型眼里的“工具”是什么很多人第一次接触 Tool Calling 会懵觉得大模型怎么能“调用”外部函数它又不是操作系统。这里要把概念掰清楚。大模型本身不执行任何代码它做的只有一件事根据你给的上下文生成一段文本。所谓工具调用本质上是你在提示词里告诉它“有这么几个工具可用每个工具叫什么名字、干什么用、需要什么参数”然后它生成的文本不是给用户看的回复而是一段符合约定格式的调用请求。这个约定格式不同厂商的 API 略有差异但核心结构是一样的工具名加参数对象。比如用户问“帮我查下订单 20250312-8871”大模型生成的可能是{name: query_order, arguments: {order_id: 20250312-8871}}。你的程序拿到这个 JSON去执行真正的查询把结果再拼回对话历史第二次请求大模型它这次生成的就是给用户的自然语言回复了。理解这一点很关键因为它决定了你调试时的思路。当工具调用失败时问题可能出在三个地方大模型没理解该调用工具、大模型理解了但参数抽错了、参数对了但你的工具执行报错了。这三个问题的排查方法完全不同后面我会细说。3.2 工具描述怎么写才能让大模型用对工具描述是 Tool Calling 里最容易被低估的环节。很多人随便写一句“查询订单”然后抱怨大模型老是调不对。实际上工具描述就是给大模型看的说明书你写得越清楚它用得越准。一份好的工具描述至少包含四部分工具名、功能说明、参数定义、使用场景。工具名要见名知意用英文小写下划线风格比如 query_order、search_logistics、check_refund_policy。功能说明用一句话讲清楚这个工具做什么不要写“处理订单相关事务”这种模糊表述要写“根据订单号查询订单的详细信息包括商品、金额、状态、下单时间”。参数定义要说明每个参数的类型、含义、是否必填最好给一个示例值。使用场景则是告诉大模型什么时候该用这个工具比如“当用户提供了订单号并且想查询订单状态时使用”。我踩过的一个坑是参数命名太随意。有一次我把参数写成id结果大模型在用户说“查一下订单 123”的时候把 123 当成了用户 ID 而不是订单 ID。后来改成order_id并且在描述里明确写“订单号通常是一串包含日期和序号的字符串”准确率立刻上去了。参数名要自解释别让大模型去猜。3.3 查询工具的实现要点查询工具本身就是一个普通的 Python 函数接收参数执行 SQL返回结果。但有几个细节要注意。第一返回值要是可序列化的结构通常是字典或者列表因为后面要转成 JSON 喂回给大模型。第二要做好异常处理查不到记录时不要抛异常而是返回一个明确的“未找到”结果让大模型知道该怎么回复用户。第三返回的字段名要清晰别用col1、col2这种用order_status、product_name这种自解释的名字。SQL 注入这个问题在练手阶段容易被忽略但习惯要养好。永远不要用字符串拼接构造 SQL用参数化查询。SQLite 的 Python 驱动支持?占位符把参数作为元组传进去就行。虽然大模型生成的参数看起来人畜无害但你不知道用户输入里会不会藏东西参数化查询是零成本的防护。还有一个实践细节是返回结果的裁剪。如果你的订单表有五十个字段全返回给大模型既浪费 token 又干扰判断。只返回跟当前任务相关的字段比如订单号、状态、金额、下单时间。这个裁剪逻辑放在工具函数里做不要指望大模型自己去过滤。3.4 对话循环的控制逻辑Tool Calling 的完整流程是一个循环不是一次请求就结束。第一轮你把用户消息和工具定义发给大模型它返回工具调用请求。你执行工具把结果追加到对话历史。第二轮你把更新后的对话历史再发给大模型它这次返回自然语言回复。如果它又返回了工具调用请求那就继续执行、继续追加直到它返回纯文本回复为止。这个循环要有终止条件不能无限转下去。通常设置一个最大轮次比如 5 轮超过就强制结束并返回兜底回复。我见过因为工具返回格式不对大模型反复调用同一个工具的情况没有轮次限制就会死循环烧 token 还出不来结果。对话历史的管理也有讲究。工具调用的请求和结果都要按特定格式追加到消息列表里不同 API 的格式要求不一样。有的要求工具结果用role: tool的消息有的要求用role: user包一层。这个必须严格按文档来格式错了大模型就理解不了上下文会重复调用或者答非所问。4. 实操过程从零把这条链路跑通4.1 环境准备与依赖安装先把环境弄干净。我建议用虚拟环境别把全局 Python 环境搞乱。Python 版本 3.9 以上都行我用的是 3.11。创建虚拟环境、激活、装依赖三步走。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install openai这里我装的是 openai 这个库因为很多国内大模型的 API 都兼容 OpenAI 的接口格式学会一套就能切换多家。如果你用的是特定厂商的 SDK按官方文档装对应的包。SQLite 不用装Python 标准库自带 sqlite3。API Key 的管理要养成好习惯别硬编码在代码里。用环境变量或者 .env 文件代码里通过 os.environ 读取。练手项目也建议这么做因为一旦养成硬编码的习惯后面写生产代码很容易出事。4.2 造一批测试工单数据没有数据就没法验证所以先造一个 SQLite 数据库建一张订单表塞几条测试数据。表结构不用复杂够用就行。import sqlite3 conn sqlite3.connect(tickets.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS orders ( order_id TEXT PRIMARY KEY, product_name TEXT, amount REAL, status TEXT, created_at TEXT, customer_name TEXT ) ) test_orders [ (20250312-8871, 激光打印机 X200, 1299.00, 已发货, 2025-03-12, 张先生), (20250310-5523, 无线键盘 K380, 199.00, 已完成, 2025-03-10, 李女士), (20250315-9902, 显示器 27寸 4K, 2199.00, 待发货, 2025-03-15, 王先生), ] cursor.executemany(INSERT OR REPLACE INTO orders VALUES (?,?,?,?,?,?), test_orders) conn.commit() conn.close()数据造好之后手动查一下确认没问题。这一步别省我见过数据库文件建错路径、表名拼错、字段类型不对的各种低级问题提前查一下能省后面半小时的排查时间。4.3 实现查询工具函数工具函数要做得健壮一点。接收 order_id查数据库返回字典。查不到就返回一个带明确标识的结果别抛异常。import sqlite3 def query_order(order_id: str) - dict: conn sqlite3.connect(tickets.db) cursor conn.cursor() cursor.execute( SELECT order_id, product_name, amount, status, created_at, customer_name FROM orders WHERE order_id ?, (order_id,) ) row cursor.fetchone() conn.close() if row is None: return {found: False, message: f未找到订单号为 {order_id} 的订单} return { found: True, order_id: row[0], product_name: row[1], amount: row[2], status: row[3], created_at: row[4], customer_name: row[5] }注意这里用了参数化查询?占位符加元组传参。返回结构里加了found字段这是给大模型看的信号让它知道查询是成功还是没找到。字段名全部自解释大模型拿到之后能直接理解每个值的含义。4.4 定义工具 Schema 并接入大模型工具 Schema 是给大模型看的说明书用 JSON 格式描述。不同 API 的字段名略有差异但结构大同小异。tools [ { type: function, function: { name: query_order, description: 根据订单号查询订单的详细信息包括商品名称、金额、订单状态、下单时间和客户姓名。当用户提供了订单号并想查询订单相关问题时使用此工具。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号格式通常为日期加序号例如 20250312-8871 } }, required: [order_id] } } } ]description 里我特意写了“当用户提供了订单号并想查询订单相关问题时使用”这是使用场景提示。parameters 里给了示例格式帮助大模型识别订单号。这些细节看起来啰嗦但实测下来对准确率提升很明显。4.5 编写完整的对话循环把上面的东西串起来就是一个完整的处理流程。核心是一个 while 循环不断请求大模型、执行工具、追加结果直到大模型返回纯文本。import json from openai import OpenAI client OpenAI(api_key你的API_KEY, base_url你的API地址) def handle_ticket(user_message: str) - str: messages [ {role: system, content: 你是一个工单处理助手负责理解用户的问题并调用工具查询信息然后用友好的语气回复用户。}, {role: user, content: user_message} ] max_rounds 5 for _ in range(max_rounds): response client.chat.completions.create( model你的模型名, messagesmessages, toolstools, tool_choiceauto ) msg response.choices[0].message if msg.tool_calls: messages.append(msg) for tool_call in msg.tool_calls: if tool_call.function.name query_order: args json.loads(tool_call.function.arguments) result query_order(args[order_id]) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) else: return msg.content return 抱歉处理这个问题时遇到了困难请稍后再试或联系人工客服。 # 测试 print(handle_ticket(我上周买的打印机订单 20250312-8871 现在什么状态了))这段代码跑通整个链路就活了。用户发一句话大模型识别出要查订单抽出订单号调用 query_order拿到结果生成回复。你不需要写任何意图识别的规则也不需要写任何字段抽取的正则这些全由大模型完成。4.6 实测效果与参数观察我用上面三条测试数据跑了几轮输入不同的问法观察大模型的调用行为。问“订单 20250312-8871 到哪了”它正确调用工具并返回“已发货”。问“我买键盘那个订单怎么样了”它没有订单号会先追问订单号这是合理的。问“帮我查下 20250310-5523”它直接调用工具返回“已完成”。有一个值得注意的现象是当用户消息里同时包含多个订单号时大模型会发起多次工具调用。比如“帮我查下 20250312-8871 和 20250310-5523 这两个订单”它会生成两个 tool_calls我的循环里用 for 遍历处理两个结果都追加回去最后它生成一条合并的回复。这个行为是自动的不需要额外配置。参数方面temperature 建议设低一点0 到 0.3 之间。工具调用需要的是稳定和准确不需要创造性。我试过 temperature 设 0.8同样的输入偶尔会抽错订单号调低之后就稳定了。max_tokens 不用设太大工单回复通常不长512 到 1024 足够。5. 常见问题与排查技巧实录5.1 大模型不调用工具直接瞎编答案这是最常见的问题。用户问订单状态大模型不调工具直接回复“您的订单正在处理中”。原因通常是工具描述不够清晰或者系统提示词没有强调要用工具。解决办法有两个一是在系统提示词里明确写“涉及订单查询必须调用 query_order 工具不要凭猜测回答”二是把工具描述写得更具体把使用场景写进去。我实测下来系统提示词里加一句“如果用户问题涉及具体订单信息必须先调用工具查询”能解决大部分情况。5.2 工具调用参数抽取错误用户说“查一下 8871 那个订单”大模型可能把 8871 当成完整订单号传进去而实际订单号是 20250312-8871。这种部分匹配的问题靠工具函数本身解决不了因为工具只认完整订单号。我的处理方式是在工具返回“未找到”之后让大模型根据上下文再追问用户完整订单号。或者在工具描述里强调“订单号是完整字符串不要截取部分数字”。如果业务上确实需要支持模糊查询那就在工具函数里加 LIKE 查询但要注意返回多条时的处理逻辑。5.3 工具返回结果格式导致大模型理解错误有一次我把查询结果直接返回成字符串大模型把整个 JSON 字符串当成了订单内容复述给用户回复里全是花括号和引号。后来改成返回结构化的字典并且在系统提示词里说明“工具返回的是结构化数据请提取有用信息用自然语言回复”。格式问题在 Tool Calling 里很关键返回给大模型的内容要干净、结构化、字段名清晰。5.4 对话循环不终止前面提过工具返回格式不对或者大模型反复调用同一个工具会导致循环不终止。除了设置最大轮次还要在追加工具结果时确保格式正确。不同 API 对 tool 消息的格式要求不同有的要求tool_call_id有的要求name字段必须严格按文档来。我建议在循环里加日志打印每一轮大模型返回的内容出问题时一眼就能看出卡在哪。5.5 常见问题速查表问题现象可能原因排查方向解决方式不调用工具直接回答工具描述模糊、系统提示词未强调检查 description 和使用场景补充系统提示词明确必须调用工具参数抽取错误参数名不清晰、缺少示例检查参数定义改参数名加示例值加格式说明返回结果被复述返回格式不结构化检查工具返回值返回字典系统提示词说明提取信息循环不终止工具结果格式错误打印每轮返回内容按 API 文档修正 tool 消息格式加最大轮次查不到订单订单号不完整或不存在检查传入参数工具返回未找到让大模型追问完整订单号5.6 几个踩坑之后的经验第一个经验是先把工具函数单独测通再接大模型。我一开始图快直接端到端跑结果工具函数里一个 SQL 字段名拼错大模型那边表现是“查询失败”排查了半天才发现是数据库层的问题。后来我养成习惯工具函数写完先手动调用几次确认返回正确再接进 Agent。第二个经验是日志要打全。每一轮发给大模型的消息、大模型返回的内容、工具执行的参数和结果全部打出来。Tool Calling 的调试本质上是看数据流没有日志就是盲调。我用的就是最简单的 print够用了。第三个经验是别在起步阶段追求多工具。我见过有人第一个项目就定义五六个工具结果大模型选择困难该调 A 的时候调了 B。先把一个工具调到 95% 准确率再加第二个。工具数量增加带来的复杂度不是线性的是组合爆炸的。第四个经验是系统提示词值得反复打磨。同样一套工具定义系统提示词写得好不好准确率能差出两成。我的系统提示词模板是角色定义 能力说明 工具使用规则 回复风格要求。这四块写清楚大模型的表现会稳定很多。6. 这条链路后续可以怎么扩展把单工具查询跑通之后扩展方向其实很自然。最直接的是加工具比如加一个查物流的工具、加一个查退款政策的工具。工具多了之后大模型会根据用户问题自动选择合适的工具这就是多工具编排的雏形。但记住我前面说的一个一个加加一个调一个。再往上是加多轮对话的记忆。现在的实现是无状态的每次请求都是独立的。如果要处理“刚才那个订单帮我退了吧”这种依赖上下文的工单就需要把历史对话维护起来。这个在消息列表里追加就行但要注意上下文长度控制太长了要做摘要或者裁剪。还有一个方向是接入真实的数据源。SQLite 换成 MySQL 或者公司的订单系统 API工具函数的实现变一下上层的 Agent 逻辑完全不用动。这就是把工具抽象出来的好处数据源换了大模型那边的体验是一致的。如果要做成真正能用的工单系统还需要考虑并发、限流、错误重试、人工兜底这些工程问题。但这些都属于“跑起来之后”的事起步阶段不用想太多。先把这条最小链路跑通你会对 AI Agent 的工作方式有一个完全不同于看文章的理解。我自己最大的体会是看再多 Tool Calling 的教程都不如自己亲手把一个查询工具接上去跑一遍来得实在。那些参数格式、消息结构、循环控制的细节只有真正跑过一遍才会变成你自己的东西。