登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

AI 工具调用为何不能直接信参数:用 Go 做函数参数校验与幂等执行

来源:17golang原创

时间:2026-08-25 15:13:34 255浏览 收藏

把大模型接进退款、发货、改价或删除数据的后台后,最容易被忽略的一层是:模型返回的函数名和参数只是一次推断,不能直接当成业务事实。更稳妥的做法是把它当作不可信请求,先经过 Go 网关的字段校验、权限判断和幂等控制,再决定是否调用真正的工具。

实践要点:
  • 工具名使用白名单,未知动作直接拒绝。
  • 金额、枚举和资源归属在服务端复核。
  • 每次动作带 request_key,重复请求返回同一结果。
  • 遇到未知状态先查询,再决定是否重试。

先把模型输出放在业务边界之外

Google 的函数调用说明把流程拆成三段:模型声明要调用哪个函数,应用解析函数名和参数,应用自己完成工具调用并把结果交回模型。也就是说,模型负责提出动作,不拥有订单服务的最终权限。结构化输出同样只解决格式问题,JSON 能被解析并不代表订单号存在、金额合理或操作者有权限。

下面做一个小型「工具调用网关」。它只允许查询订单和申请退款两个动作,其中退款还要满足金额上限、订单归属和请求键唯一。

AI 工具调用参数经过 Go 白名单、字段校验和权限闸门后才进入受限工具
模型建议与业务动作之间,必须有一层可审计的 Go 校验闸门。

准备一个可重复运行的 Go 小项目

示例只使用标准库,项目存储路径可以简化到单个文件:

mkdir ai-tool-guard
cd ai-tool-guard
go mod init example.com/ai-tool-guard

真实项目里,模型 SDK 只负责把函数调用解析成结构体;本例直接构造同样的输入,先把业务边界测清楚。

用白名单和结构体限制工具参数

不要根据模型返回的字符串动态寻找函数,更不要让参数里的字段名决定数据库更新范围。工具名、必填字段、枚举值和数值边界都应该写在服务端。

package main

import (
    "errors"
    "fmt"
    "math"
    "strings"
)

type ToolCall struct {
    Name string            `json:"name"`
    Args map[string]string `json:"args"`
}

type Result struct {
    RequestKey string
    Status     string
    Message    string
}

func validate(call ToolCall) error {
    if call.Name != "order_lookup" && call.Name != "refund_request" {
        return errors.New("tool is not allowed")
    }
    if strings.TrimSpace(call.Args["order_id"]) == "" {
        return errors.New("order_id is required")
    }
    if call.Name == "refund_request" {
        cents, err := parseCents(call.Args["amount"])
        if err != nil || cents  50000 {
            return errors.New("refund amount is out of range")
        }
    }
    return nil
}

func parseCents(s string) (int64, error) {
    var yuan float64
    if _, err := fmt.Sscan(s, &yuan); err != nil || math.IsNaN(yuan) {
        return 0, errors.New("invalid amount")
    }
    return int64(math.Round(yuan * 100)), nil
}

这里的金额只是演示输入层校验,生产代码应使用整数分或 decimal 类型,不要让浮点数直接参与结算。校验失败时只返回可记录的原因,不把原始模型输出原样拼进 SQL、命令或内部 URL。

用 request_key 把重复动作变成一次

网络重试、用户重复点击和模型再次提出同一动作,都会让退款接口收到重复请求。幂等键应由业务请求生成并持久化,而不是每次重试临时生成。下面用内存 map 表示最小流程,正式环境应换成带唯一约束的数据库表或可靠的幂等存储。

var completed = map[string]Result{}

func dispatch(call ToolCall, requestKey string) (Result, error) {
    if old, ok := completed[requestKey]; ok {
        return old, nil
    }
    if err := validate(call); err != nil {
        return Result{}, err
    }

    // 真实项目:这里还要查询订单归属、当前状态和操作者权限。
    result := Result{
        RequestKey: requestKey,
        Status:     "accepted",
        Message:    "validated tool call",
    }
    completed[requestKey] = result
    return result, nil
}

关键点不在 map,而在“检查并写入”必须具备原子性:多实例服务要让数据库唯一索引或分布式存储承担这件事。发现同一个 request_key 处于 processing 状态时,也不能立即再次产生副作用,应先查询原动作的最终状态。

把失败重试分成可重试和未知状态

参数错误、权限不足和业务状态不允许,属于不可重试错误;连接超时则不一定代表工具没有执行成功。对退款、发货这类动作,超时后的安全动作是查询 request_key 对应的执行记录,而不是盲目再发一次。

type RetryClass string

const (
    NoRetry       RetryClass = "no_retry"
    QueryThenRetry RetryClass = "query_then_retry"
)

func classify(err error, sent bool) RetryClass {
    if err == nil || !sent {
        return NoRetry
    }
    return QueryThenRetry
}

这一步也决定了日志怎么写:记录模型请求的 trace_id、request_key、校验结果、工具响应状态和重试分类,避免只留下“AI 调用失败”这种无法复盘的描述。

AI 工具请求携带 request_key,重复请求读取既有结果,未知状态先查询再重试
幂等键把“重试”从再次执行改成查询已有动作。

本地运行时核对四个结果

可以补一个测试表,分别覆盖未知工具、超额退款、相同 request_key 和超时后的查询分支。最少要确认下面四件事:

  • 未知工具名被拒绝,且没有进入业务服务。
  • 金额为 0、负数或超过上限时被拒绝。
  • 相同 request_key 第二次请求返回第一次的结果。
  • 已发出但结果未知的请求先查状态,不直接产生第二次副作用。

如果把网关接到真实模型,先记录模型返回的原始调用结构,再把解析后的 ToolCall 送入同一套校验函数。不要为“来自模型”单独开一条更宽的权限路径。

上线前的边界清单

小项目接入生产系统前,还要补上认证、资源归属、敏感字段脱敏、审计留存和并发测试。模型可以帮助理解用户意图,但不能替代订单状态机、权限系统和金额校验。最稳妥的上线顺序是先接只读查询,再接可撤销动作,最后才评估不可逆操作。

常见问题与边界

结构化 JSON 合法就可以直接入库吗?

不可以。格式合法只说明字段能被解析,仍要校验业务语义、权限、资源存在性和当前状态。

超时后重新发送工具调用安全吗?

不能一概而论。先用 request_key 查询动作状态;只有确认没有执行且业务允许时,才进入重试。

为什么要把工具名写成白名单?

白名单能把模型可请求的能力限制在明确范围内,也方便审计和版本变更,避免新增函数后被自动暴露。

验收结论

一个可用的 AI 工具调用链,不是把模型输出接到函数入口,而是把它当作待审核的请求:先校验,再查权限;先落幂等记录,再产生副作用;遇到未知结果,先查询再决定。这样即使模型参数不完整、用户重复提交或网络发生超时,业务仍有明确的拒绝、恢复和复盘路径。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>