news 2026/9/25 22:19:12

FDE 实战:给 AI 加上 Tool Calling,让模型真正操作业务系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FDE 实战:给 AI 加上 Tool Calling,让模型真正操作业务系统

FDE 实战:给 AI 加上 Tool Calling,让模型真正操作业务系统

专栏:《AI FDE 实战:从 Demo 到生产》|第 11 篇 / 共 18 篇
本篇目标:让模型通过受控工具查询订单、形成工单草稿,并由独立的人工确认端点完成本地模拟提交。
本篇产物:工具契约、有界 Responses 调用循环、SQLite 幂等提交逻辑、FastAPI 确认端点与失败路径自检。
星河设备、订单、用户与工单均为虚构教学数据;实验不会连接真实 CRM、发送邮件或创建外部工单。

客服已经能够通过助手查询政策,下一句话往往是:“那你顺便把这个问题登记成工单吧。”

从用户体验看,这只是多了一句自然语言;从系统责任看,却发生了重要变化。之前的输出主要是信息,现在的输出可能改变业务记录。助手说“已经提交”之后,用户会期待工单号、处理状态和后续跟进。语言流畅不再足以证明任务完成。

本篇以订单 SO-1042 的设备无法启动为例,完成一个很小但完整的闭环:查询可见订单,生成待确认草稿,展示具体内容,用户确认后写入本地模拟工单。模型参与理解与草拟,应用负责身份、授权、执行边界和状态。读者可以运行所有本地实验,也能沿着明确接口扩展到真实系统。

一、Tool Calling 不是让模型直接获得数据库权限

工具调用的基本过程是:应用向模型说明可用工具及其参数,模型提出某个工具调用请求,应用解析并验证请求,执行自己控制的业务函数,再把结果返回模型。模型随后可以组织回答,或在允许的范围内请求下一步。

这里最重要的主语是“应用执行”。模型产生的函数名和 JSON 参数,只是一个需要处理的请求。它不能因为格式正确就自动成为数据库命令,也不能因为用户说“我有权限”就改变服务端身份。执行者必须保留对工具白名单、参数与业务条件的控制。

图 2:模型提出调用,应用验证与执行,业务系统返回可核验结果。

例如,模型可以请求查询某个订单,但应用要检查该订单是否属于当前租户、当前角色是否可以查看。模型可以整理工单摘要,但创建工单之前,应用还要确认用户批准的就是这份内容。工具调用让语言接上业务能力,并没有替代原有权限与事务设计。

OpenAI 的 Responses API 使用专门的函数调用输出项表达请求,应用通过对应的工具结果项继续对话。本文采用这一接口形态,避免把其他 API 的消息结构直接混用。参考:OpenAI Function Calling 官方文档

二、先画动作边界,再写工具 Schema

我们只给模型两个工具:lookup_order查询订单,draft_ticket生成草稿。正式确认函数confirm_ticket不进入模型工具列表,而由用户界面的独立确认请求触发。这个划分让“提出内容”和“批准执行”在代码里也有清楚边界。

只读工具也不是零风险。查询可能暴露客户信息,消耗上游配额,或者返回含有不可信文本的备注。因此只读路径仍然需要鉴权、结果裁剪和超时。它与写入工具的差别主要在业务副作用,不意味着可以跳过所有检查。

草稿工具虽然写入本地草稿表,但不创建正式工单。用户界面必须显示“待确认”,不能为了让演示顺畅而把它写成“已提交”。在本篇代码里,草稿包含订单、摘要、失效时间和内容哈希;正式工单则具有独立工单号,并由确认端点返回。

企业可以根据风险设计不同确认要求,但不能把“是否确认”留给模型临时判断。某些低影响操作可以在明确授权下自动执行,退款或大范围更新则可能需要额外审批。FDE 的工作是把业务决定落实为稳定的执行条件,而不是每次依靠提示词猜测。

三、工具接口应贴近用户任务,而不是暴露内部通用能力

一个叫作execute_sql的工具看起来灵活,却把数据库结构、查询语法和权限风险一起交给了模型。相比之下,lookup_order(order_id)清楚表达一个业务动作,服务端可以固定返回字段、查询条件和错误行为,读者也更容易测试它是否符合承诺。

同样,不需要给模型一个可以任意发送 HTTP 请求的通用工具来完成售后登记。真实业务里,允许调用哪些系统、哪些路径、什么请求体,应当由应用控制。范围越小,模型选择越容易,异常处理和审计也越清楚。

工具描述应说明用途、必要输入、返回状态与副作用。不要只写“处理订单”,因为模型无法据此判断它是查询、取消还是修改。草稿工具尤其要明确“生成待确认内容,不提交正式工单”,让模型和界面都围绕同一个状态解释结果。

参数尽量使用用户任务中已经存在的业务标识。租户与当前用户不应该作为模型自由填写的参数;它们由服务端上下文注入。若模型提供一个tenant_id,应用应拒绝这个多余字段,而不是把它视为用户有意切换租户的依据。

四、严格 Schema 管结构,授权逻辑管能否执行

Responses 工具定义中可以明确启用严格模式,并为对象设置允许字段与必填字段。本文的查询工具只接收一个字符串订单号,草稿工具接收订单号和摘要。这样可以降低形状错误,但服务端仍然进行自己的验证。

lookup_tool={"type":"function","name":"lookup_order","description":"查询当前用户可见订单,不改变业务数据。","strict":True,"parameters":{"type":"object","properties":{"order_id":{"type":"string"}},"required":["order_id"],"additionalProperties":False,},}

即使订单号是合法字符串,也可能属于另一个客户;即使摘要长度符合要求,也可能包含用户没有批准的承诺。结构约束不等于业务正确性,更不等于访问授权。把这两类责任混为一谈,是工具调用系统里很容易出现的误解。

严格输出也不能消除网络错误、响应不完整或模型拒绝等情况。应用必须检查实际返回状态,并对无法继续的结果提供明确出口。本文循环在模型响应未完成时返回不可用状态,不会把空文本当成任务已经完成。参考:OpenAI Structured Outputs

五、订单查询先证明“看得见”,再返回最少必要信息

本篇使用UserContext保存租户、用户和角色。查询 SQL 同时限定租户与订单号,只返回订单号、产品和状态。为了演示越权路径,数据库里还有另一个租户的 SO-2042。租户甲的客服查询它时,与查询一个不存在订单得到同样的外部状态。

为什么不告诉用户“订单存在,但属于其他客户”?因为存在性也可能是敏感信息。统一成“未找到或不可见”,能避免接口成为枚举其他客户对象的工具。内部审计可以保留更具体原因,但不必把它们直接交给用户或模型。

返回值也不需要包含整行数据库记录。为当前任务选择必要字段,减少模型接触无关个人信息,也降低上下文成本。将来确实需要收件地址或联系方式时,再为明确任务增加字段与访问条件,避免一开始就把整个客户对象倾倒进对话。

身份验证与业务授权是不同步骤。识别某人已经登录,不等于允许他访问每个订单;一个租户的主管,也不自动成为所有租户的管理员。FastAPI 可以通过依赖提供当前身份,但依赖返回之后,业务函数仍然需要对象范围判断。参考:FastAPI Security Tutorial

六、正确处理 Responses 中的调用与结果对应关系

在 Responses 返回内容中,应用检查类型为function_call的项,读取工具名称、序列化参数以及call_id。执行后创建function_call_output,使用相同的调用标识关联结果。不能用工具名称代替调用标识,因为一次流程可能多次调用同一工具。

图 3:应用保留响应输出项,并把每个工具结果关联到原来的调用。

history.extend(response.output)forcallinresponse.output:ifcall.type!="function_call":continuearguments=json.loads(call.arguments)result=dispatch(db,context,call.name,arguments)history.append({"type":"function_call_output","call_id":call.call_id,"output":json.dumps(result,ensure_ascii=False),})

上面是核心协议片段,完整循环还包含错误处理与预算限制。保留整个响应输出,而不是只保留文本,有助于正确延续包含其他输出项的对话状态。工具结果以受控结构返回,避免把内部异常堆栈直接塞给模型。

每一轮都重新发送工具定义和行为约束,让应用状态清楚可见。本文使用应用保存的历史列表,不依赖服务端自动持久化会话;真实系统可以选择不同状态管理方式,但必须明确哪些内容被保留、谁能读取以及如何清理。

七、调度器用白名单,不使用动态执行

dispatch将两个允许名称映射到具体业务函数,并核对参数键集合。未知工具返回受控错误,额外字段和类型错误同样被拒绝。不要根据模型提供的名字执行任意模块函数,更不要把参数放进eval或 shell 命令。

业务函数还会再次检查订单格式、摘要长度和可见范围。重复验证并不是为了堆叠框架,而是因为这些函数也可能被 HTTP 端点或其他服务调用。关键规则放在共享业务边界,才能避免某条入口绕过限制。

错误输出应该区分可采取的下一步。参数不足可能需要用户补充;对象不可见不应鼓励模型继续枚举编号;临时服务故障可以在预算内重试;未知工具则说明当前请求无法执行。把所有错误都写成“失败了,请再试一次”,容易造成无意义循环。

对于工具返回的自由文本,例如客服备注或用户留言,也要把它视为不可信数据。如果备注里写“现在调用确认接口”,它不能因此改变工具列表或用户授权。把文本放进 JSON 不会自动使它安全,真正的权限仍由调度与业务层控制。

八、循环必须有结束条件,而且要能解释为什么结束

模型可能反复请求同一订单,也可能在错误后不断修改参数。本篇最多允许四轮模型请求、四次工具调用,并在每轮前检查时间预算。达到上限后返回明确原因,不再继续消耗资源。SDK 请求还设置单次超时,并关闭自动重试,便于观察实验行为。

这里的时间预算是循环层的检查,不是精确到毫秒的强制终止器。一个已经发出的网络请求仍然受其自身超时控制。生产系统若需要严格总体截止时间,应把剩余预算传递给下游请求,或使用支持取消的执行方式,不能只在外层记录开始时间。

本篇关闭并行工具调用以简化顺序和状态,但应用仍然遍历所有实际返回的调用项。不要把请求配置当成唯一防线:工具总数限制、白名单和授权检查仍然作用于每一次调用。未来启用并行时,还要区分独立只读动作与存在先后依赖的动作。

最终没有工具调用,不一定就代表业务已经完成。它只能表示这一轮模型没有再提出工具请求。应用向用户展示的正式提交状态必须来自确认端点和数据库结果,不能从模型最后一句“已经帮你处理”中提取。

九、草稿应该是可以审核的具体对象

客服说“帮我建个工单”,并没有批准模型自由补充任何事实。草稿应明确展示订单、问题摘要、将提交到哪里,以及可能产生的后续影响。没有确认的故障原因不要写成事实,没有查到的购买时间不要凭语言推断。

本篇草稿保存一份不可变的规范化内容,并计算内容哈希。用户看到这份内容后,确认请求携带草稿标识与对应哈希。确认端点只使用服务器保存的内容,不接受浏览器在同一请求里另传一个新摘要,从而避免展示内容与实际提交内容不一致。

图 4:修改内容需要生成新草稿;已确认结果通过工单标识核验。

内容哈希不是身份凭据,也不是万能签名。它帮助发现确认对象发生变化,真正的访问限制仍来自会话身份、草稿所有者、租户和当前权限。攻击者即使知道哈希,也不应该因此获得确认别人工单的权力。

草稿还需要失效时间。用户上午打开确认卡片,下午订单状态或权限已经变化,原先适合的操作可能不再成立。本文设置教学用的十分钟有效期,并在确认时重新检查订单可见性。真实有效期应根据业务变化速度和用户工作方式决定。

十、人工确认必须是服务端可以区分的执行路径

最薄弱的确认方式是让模型询问“是否确认”,然后自己识别下一条“好的”,再调用写入工具。对话里的确认可能指向不同内容,也可能被无关文本干扰。如果系统声称需要人工确认,就应把确认对象和身份绑定到明确请求。

本篇提供POST /api/tickets/confirm,只接受草稿标识、内容哈希和幂等键。模型工具列表里没有这个端点。实际集成时,前端根据服务端返回的草稿对象展示卡片,由用户点击按钮触发请求,而不是让模型生成一段可执行的任意链接。

本地 HTTP 实验使用公开固定的演示令牌,把请求映射到两个虚构用户。这只是为了运行身份分支,不能作为真实认证。任何知道令牌的人都能冒充该演示用户,所以启动命令只绑定本机地址;生产接入必须替换为经过验证的身份提供方。

若真实应用采用浏览器 Cookie 会话,还要根据认证方式处理跨站请求等问题,并保持确认接口的访问控制。本文没有实现完整生产会话、安全表单或企业单点登录,不能因为端点能返回工单号就直接将其公开部署。

十一、幂等解决的是重复执行,不是所有一致性问题

用户连续点击两次按钮、浏览器重发请求、网关在超时后重试,都可能把一次确认变成多次到达。应用需要识别这些请求属于同一个业务意图,并返回同一个结果。本篇使用租户内的幂等键,同时保存该键对应的草稿与内容指纹。

如果同一个幂等键再次携带相同草稿和内容,返回原工单号;如果它被拿来确认另一份草稿,返回冲突。不能仅凭“这个键出现过”就直接返回旧结果,否则用户可能把不同请求错误合并,甚至收到不属于当前确认内容的响应。

数据库还为草稿与正式工单建立唯一关系。即使客户端换了一个幂等键,同一份有效草稿也不会创建第二张工单。两层约束分别守住请求重放与同一业务草稿重复提交,避免把全部责任压在前端按钮禁用上。

已经成功确认的同键重放,即使发生在草稿失效时间之后,也可以返回已有结果,因为没有发生新的业务执行。但仍需重新检查当前身份和对象可见性。相反,一份从未确认过的过期草稿应被拒绝,要求重新生成并审核。

十二、事务把检查和写入放在同一个受控范围

如果应用先查“没有工单”,再在另一个事务里插入,两次并发请求都可能看到不存在,然后各自创建一条记录。唯一约束可以阻止部分重复,但应用仍需要正确处理冲突,并把幂等记录与工单结果一起提交。

本篇 SQLite 确认逻辑使用BEGIN IMMEDIATE,在事务中读取草稿、检查当前权限、核验内容、读取幂等记录,最后写入工单和幂等映射。任意步骤失败就回滚。这个小实验选择了容易解释的写入序列,不追求高并发吞吐。参考:SQLite Transactions

数据库事务只覆盖这个本地数据库。若真实工单位于第三方 CRM,先调用外部接口再写本地记录,可能遇到外部成功而本地失败;先写本地又可能遇到外部失败。此时需要结合外部幂等能力、状态查询、任务表或事务发件箱等机制设计恢复路径。

尤其不要把超时直接解释成失败。请求可能已经被外部系统执行,只是响应丢失。没有查明状态就重发写操作,可能制造重复工单。可靠做法是使用稳定的业务请求标识查询或恢复,必要时将状态标为待确认并交给人工处理。

十三、让界面显示业务事实,而不是模型口吻

聊天文本可以解释结果,但状态卡片应由结构化业务结果驱动。查到订单显示订单字段;生成草稿显示待确认内容;确认成功显示工单号;结果未知显示正在核实或需要人工介入。不要根据文本里是否出现“成功”来改变按钮或流程状态。

确认成功后,用户应能查看当时提交的内容和对应工单标识。如果后续允许编辑工单,要把编辑作为新的业务动作处理,不能悄悄修改原先被确认的快照。否则审计人员无法解释用户究竟批准了什么。

错误信息同样应服务于下一步。草稿过期时提供重新生成入口;内容变化时要求再次审核;当前权限不足时说明无法继续;业务服务不可用时保留草稿并避免宣称提交完成。不同错误对应不同操作,简单弹出“系统错误”只会诱使用户不断点击。

把模型的自由文本限制在它擅长的解释范围,把关键状态交给业务结果,是一种很实用的架构分工。它让模型偶尔使用过于积极的措辞时,界面仍然有明确的事实依据,也方便客服与工程人员共同排查问题。

十四、运行本地实验,观察失败路径

完整代码在配套目录中。先运行纯标准库的领域实验和工具循环实验,再运行依赖 FastAPI 的 HTTP 检查。三个脚本使用临时 SQLite 文件进行自检,完成后自动清理,不会碰用户现有数据库。

cdoutputs/11-tool-calling/code python ticket_lab.py python test_loop.py python test_http.py

实际运行结果为:领域逻辑十一项检查通过,离线工具协议与有界循环九项检查通过,HTTP 身份与确认边界五项检查通过。它们覆盖正常查询、跨租户不可见、草稿不产生正式工单、内容变化、过期、重复确认、幂等键冲突、空响应以及角色撤销等分支。

图 5:正常路径证明可以工作,失败路径证明哪些操作必须停止。

离线循环使用固定响应对象模拟模型输出,只能验证程序如何处理协议,不证明真实模型一定选择正确工具。真实 Responses 示例保存在responses_loop.py,需要配置自己的服务端密钥与可用模型后运行。本文交付时没有发起这条真实模型调用。

本地交互端点可以通过uvicorn app:app --host 127.0.0.1 --port 8011启动。它会在练习目录创建模拟数据库,使用方法见配套说明。不要把这套公开演示令牌带到生产,也不要把模拟工单号当作真实企业系统的提交凭证。

十五、把它接回售后助手时,接口应该如何组合

第 07 篇的聊天接口已经有回答、状态、引用和追踪标识。本篇工具循环可以成为聊天服务内部的一个组件:服务端取得可信身份后传入上下文,模型查询订单或生成草稿,返回时将草稿对象作为明确的附加结构交给界面。

RAG 与订单工具负责不同事实。政策检索解释申请材料与适用条款,订单查询提供订单状态和产品信息。不要让模型把检索到的样例订单当作当前订单,也不要把订单备注当作正式政策。每类结果保留来源与用途,能降低信息混淆。

本文提供的是独立可组合实验,尚未把所有文件自动接入前面章节的应用。集成时至少需要统一错误映射、身份依赖、追踪标识和数据存储,并为聊天到确认的完整路径增加端到端检查。仅把函数复制过去,不能代表整个流程已经验收。

工具数量增加时,先按业务任务控制可用集合,而不是一次性暴露所有内部接口。当前只处理售后登记,就没有必要让模型同时看到批量导出和退款操作。范围清楚的工具集更容易评估,也使权限变更能够落实到具体动作。

十六、审计需要记录什么,才足以解释一次提交

一次正式写入至少需要关联发起用户、租户、草稿标识、被确认内容、确认时间、幂等键与业务结果。模型调用和工具调用可以通过追踪标识关联,但不要把模型文本当作唯一审计记录。它可能省略关键字段,也可能使用与真实执行不一致的措辞。

图 6:确认内容与正式结果之间建立稳定关联,才能解释和处理争议。

本篇数据库保存草稿、工单和幂等映射,足以演示对象之间的关系,但没有实现完整的不可变审计事件表。生产系统还需要根据企业要求记录操作时间、结果类别和必要上下文,并限制审计记录的访问与修改权限。日志有内容,不等于已经满足审计要求。

敏感内容不宜在每个链路日志里重复保存。可以保留结构化标识、状态、耗时和错误类别,原始工单内容放在受控业务存储中。调查人员通过授权方式关联查看,避免调试平台意外成为另一套客户信息数据库。

还要记录“没有执行”的原因。越权被阻止、草稿过期、内容变化和幂等冲突,都是证明控制生效的事件。只记录成功操作,会让团队无法区分用户没有尝试、系统正确拒绝与请求根本没有到达。

十七、实战练习:主动制造一次重复与一次内容变化

先创建草稿并复制确认请求,连续发送两次,检查两个响应里的工单号是否一致。再更换幂等键重复确认同一草稿,核对数据库正式工单数量仍然为一。这个练习可以直接看见数据库约束比单纯禁用按钮更可靠的地方。

接着把确认请求中的内容哈希改成另一个字符串,观察接口拒绝。再生成一份新摘要,却复用旧幂等键,观察冲突。两种失败看起来相似,但原因不同:前者确认对象发生变化,后者把不同业务请求错误绑定到同一个重放标识。

第三个练习使用另一个租户的身份确认草稿。即使你拥有正确草稿标识和哈希,也应被拒绝。随后去掉用户角色,再尝试查询和确认,检查共享业务函数是否对两条入口保持一致。如果只在聊天接口检查角色,直接调用确认端点就可能绕过限制。

最后让模拟模型持续请求查询,观察循环在预算上限停止,并保留清楚的结束原因。尝试返回未知工具或无法解析的参数,确认它们不会变成任意代码执行,也不会让系统无限等待。可靠的工具调用,不仅会走通成功分支,也知道何时停下来。

FDE Thinking:行动能力越强,越需要清楚的责任归属

为什么不把确认工具直接交给模型?因为这个教学阶段的业务约定是人工审核后提交。把写入工具排除在模型能力之外,能够让约定成为可检查的程序边界,而不只是提示词里的愿望。未来改变自动化范围时,也必须先改变业务授权与验收条件。

为什么保留草稿对象,而不只显示一段聊天文本?因为对象可以绑定版本、所有者、内容和结果,可以做失效、重放和审计。聊天文字适合解释,却难以独立承担这些状态管理责任。具体对象使用户确认的东西与系统执行的东西保持一致。

为什么明明是 AI 教程,却花很多篇幅讨论事务和幂等?因为用户的工作结果发生在业务系统里。大模型让输入方式变得灵活,但没有消除分布式失败、重复请求和权限变化。能够把这些普通而关键的工程问题处理清楚,才是 AI 从演示走向交付的基础。

到这里,售后助手已经拥有两类能力:检索知识,并通过受控工具处理业务任务。下一篇将讨论什么时候需要 Agent,以及如何用有界工作流组织这些能力。先守住每个动作的输入、授权和结果,再谈更复杂的自主执行,系统才有可理解的成长路径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 22:02:35

虚拟机Windows密码忘了怎么办?NTPWEdit离线重置SAM实战

1. 虚拟机里忘记Windows密码这件事,到底该怎么收场手头有一台虚拟机,里面跑着一个Windows系统,可能是用来做测试的、可能是很久以前搭的实验环境,突然某天开机发现登录密码想不起来了。这种事在运维和测试圈子里太常见了——虚拟机…

作者头像 李华
网站建设 2026/9/25 21:45:48

【Qt】Qt 入门:Qt 初识与开发环境搭建,一篇文章带你上手 Qt

🔥 个人主页: Mercury 🍉 学习方向: C/C方向学习者 ⭐ 人生格言: 给时光以生命,而不是给生命以时光 ​ 目录 一、Qt 背景介绍 1.1 什么是 Qt1.2 Qt 的发展史1.3 Qt 支持的平台1.4 Qt 版本与许可证1.5 Qt …

作者头像 李华
网站建设 2026/9/25 21:43:30

GUI-Owl-1.5实测:多模态GUI智能体如何突破自动化脚本脆弱性

用了两周把 GUI-Owl-1.5 拉下来跑通,又把几个真实项目里的任务喂进去试了一遍,有些话想写出来。做 GUI 自动化这行当久了,最大的感受就是:脚本不脆弱才叫新闻。换台显示器分辨率,坐标全偏;UI 改个版式&…

作者头像 李华