news 2026/9/28 4:26:25

深入理解 Function Calling:让 AI Agent Harness Engineering 精准操作外部系统的底层逻辑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解 Function Calling:让 AI Agent Harness Engineering 精准操作外部系统的底层逻辑

1. 为什么 Function Calling 是 Agent 操作外部系统的关键

Function Calling(函数调用)是大模型原生支持的一种结构化输出能力:模型不再只吐自然语言,而是按你预先定义的 JSON Schema,输出「要调用哪个函数、传什么参数」。它让 AI Agent 从「只会聊天」变成「能真正动手」——查数据库、调接口、改工单、控设备,都靠这一层协议打通。

在 AI Agent Harness Engineering(智能体工程化)里,Harness 指的是包裹在模型外面的那套「驾驭层」:工具注册、参数校验、权限控制、多轮调度、日志审计。Function Calling 就是这套驾驭层与外部系统之间的标准接口。没有它,你只能靠 Prompt 诱导模型输出固定格式,格式错误率高、参数经常缺字段;有了它,模型输出受约束解码限制,结构合法性能到 90% 以上。

这篇面向需要为 Agent 接入工具调用的开发者。我会先讲清楚底层机制,再给一套可直接复制的config.toml与settings.json骨架,然后通过 TaoToken 统一 Key/API 通道完成一次真实的工具调用配置,最后发起一次外部系统调用并检查返回结果与日志。全程可跟做,不需要你已经有现成的 Agent 框架。

适合谁:正在给 Agent 接工具的后端/全栈开发者、做企业内部助理或智能客服的工程同学、想把 RPA 和 LLM 结合起来的自动化玩家。前置知识只需要你会写一点 Python 或 Node,能看懂 JSON 和 TOML 配置。

2. 前置准备:用 TaoToken 统一 Key 与 API 通道

在写配置之前,先把「模型从哪来」这件事解决掉。Function Calling 要求模型本身支持工具调用能力,不同厂商的接口字段、返回结构、鉴权方式都不一样。如果每个模型都单独接一遍,Harness 层会变得非常难维护。

我的做法是用 TaoToken 作为统一入口:一个 Key、一个 API 地址,兼容主流模型的对话与工具调用接口。这样 Harness 层只需要对接一套协议,切换模型时改配置即可,不用动业务代码。

你需要准备的东西:

  • 一个 TaoToken 账号,登录后在控制台创建 API Key;
  • 本地能跑 Python 3.9+ 或 Node 18+;
  • 一个你想让 Agent 操作的外部系统接口(本文用一个模拟的「工单查询」HTTP 接口演示)。

关键地址先记下来,后面配置里会用到:

  • 官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
  • API 基地址:https://taotoken.net/api
  • 创建 Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite
  • 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite

注意:API 基地址不要加 UTM 参数,直接写https://taotoken.net/api即可,否则部分 SDK 会把查询串拼进请求路径导致 404。

拿到 Key 之后,先别急着写 Agent。用一条 curl 确认通道是通的,这一步能帮你排除掉 80% 的「配置没错但就是调不通」问题:

curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "只回复两个字:通了"}] }'

返回里能看到choices[0].message.content就说明 Key 和通道都正常。如果返回 401,检查 Key 有没有复制完整;返回 404,检查 base URL 是不是被加了多余路径。

3. 可复制配置:config.toml 与 settings.json 骨架

Harness Engineering 的核心思路是「配置与代码分离」:模型通道、工具元数据、权限策略都放在配置文件里,代码只负责调度。下面这套骨架你可以直接抄。

3.1 config.toml:模型通道与运行参数

# config.toml [llm] # 统一走 TaoToken 通道,切换模型只改 model 字段 base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读取,不要硬编码 model = "gpt-4o-mini" timeout_seconds = 30 max_retries = 2 [agent] max_tool_rounds = 5 # 多轮工具调用上限,防止死循环 tool_choice = "auto" # auto / required / none parallel_tool_calls = true # 允许一次返回多个工具调用 [logging] level = "INFO" # 每次工具调用都落盘,排障时按 trace_id 检索 file = "./logs/agent_tool_calls.log" record_arguments = true record_result = true [tools.registry] # 工具元数据文件,Harness 启动时加载 path = "./settings.json"

这里几个参数值得展开说。max_tool_rounds是保命参数,模型偶尔会陷入「调用→结果不理想→再调用」的循环,设成 5 基本够用。parallel_tool_calls打开后,模型可以在一次响应里返回多个工具调用请求,Harness 并行执行,能明显降低多工具场景的总耗时。record_arguments建议一直开着,参数错误是 Function Calling 最高频的故障,没有参数日志根本没法定位。

3.2 settings.json:工具元数据与权限

{ "tools": [ { "name": "query_ticket_status", "description": "查询指定工单的处理状态、处理人和预计完成时间。仅允许查询用户自己的工单。", "parameters": { "type": "object", "properties": { "ticket_id": { "type": "string", "description": "工单ID,格式为 TK- 加6位数字,例如 TK-123456" }, "user_id": { "type": "string", "description": "发起查询的用户ID,格式为 U- 加数字,用于权限校验" } }, "required": ["ticket_id", "user_id"] }, "endpoint": "http://127.0.0.1:8000/api/ticket/status", "method": "POST", "required_permission": "ticket:query", "timeout_seconds": 8 } ], "permissions": { "default_user": ["ticket:query"] } }

工具描述(description)的写法直接决定模型选工具的准确率。我踩过的坑是:描述写得太笼统,比如「查询工单」,模型在有多个相似工具时会乱选。正确做法是把「能做什么、不能做什么、参数格式示例」都写进去,像上面ticket_id的格式说明,能显著降低参数生成错误。

required_permission是 Harness 层的权限闸门。模型可以「想」调用任何工具,但真正执行前必须过权限校验。高风险操作(转账、删除、改权限)建议再加一道人工确认,不要完全交给模型判断。

4. 验证请求:发起一次外部系统调用并检查结果

配置就绪后,写一个最小 Harness 跑通全链路。下面这段 Python 用 OpenAI 兼容 SDK 对接 TaoToken 通道,读取上面的配置,完成一次真实的工具调用。

4.1 最小 Harness 实现

# harness.py import json, os, logging, tomllib, requests from openai import OpenAI # 读取配置 with open("config.toml", "rb") as f: cfg = tomllib.load(f) logging.basicConfig( level=cfg["logging"]["level"], filename=cfg["logging"]["file"], format="%(asctime)s %(levelname)s %(message)s" ) client = OpenAI( base_url=cfg["llm"]["base_url"], api_key=os.environ[cfg["llm"]["api_key_env"]], timeout=cfg["llm"]["timeout_seconds"], ) with open(cfg["tools"]["registry"]["path"]) as f: registry = json.load(f) TOOLS = [{"type": "function", "function": { "name": t["name"], "description": t["description"], "parameters": t["parameters"] }} for t in registry["tools"]] def execute_tool(name, args): """执行工具调用,带权限校验和日志""" tool = next(t for t in registry["tools"] if t["name"] == name) perm = tool.get("required_permission") if perm and perm not in registry["permissions"]["default_user"]: logging.warning("permission denied: %s", name) return {"error": f"permission denied: {perm}"} logging.info("tool_call name=%s args=%s", name, json.dumps(args, ensure_ascii=False)) resp = requests.request( tool["method"], tool["endpoint"], json=args, timeout=tool["timeout_seconds"] ) result = resp.json() logging.info("tool_result name=%s result=%s", name, json.dumps(result, ensure_ascii=False)) return result def run(user_query): messages = [{"role": "user", "content": user_query}] for _ in range(cfg["agent"]["max_tool_rounds"]): resp = client.chat.completions.create( model=cfg["llm"]["model"], messages=messages, tools=TOOLS, tool_choice=cfg["agent"]["tool_choice"], ) msg = resp.choices[0].message if not msg.tool_calls: return msg.content messages.append(msg) for call in msg.tool_calls: args = json.loads(call.function.arguments) result = execute_tool(call.function.name, args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False), }) return "达到最大工具调用轮数,任务未完成" if __name__ == "__main__": print(run("帮我查一下工单 TK-123456 的状态,我是用户 U-7890"))

4.2 模拟外部系统

为了验证链路,起一个本地 mock 服务当「外部系统」:

# mock_server.py from fastapi import FastAPI app = FastAPI() @app.post("/api/ticket/status") def ticket_status(payload: dict): return { "ticket_id": payload.get("ticket_id"), "status": "处理中", "handler": "张三", "estimated_completion": "2025-06-01 18:00:00" }

启动:uvicorn mock_server:app --port 8000,然后跑python harness.py。

4.3 检查返回结果与日志

预期输出类似:

工单 TK-123456 目前状态为「处理中」,处理人是张三,预计完成时间为 2025-06-01 18:00:00。

同时logs/agent_tool_calls.log里应该能看到两条记录:一条tool_call记录模型生成的参数,一条tool_result记录外部系统返回。这两条日志是排障的核心依据——参数对不对看第一条,外部系统返回什么看第二条。

如果模型没有触发工具调用,直接回了自然语言,先检查tool_choice是不是被设成了none,再检查工具描述是否足够清晰。如果触发了但参数是空的,多半是required字段没写全,或者参数描述缺少格式示例。

5. 本篇常见错误排查

5.1 401 / 403:鉴权失败

最常见的原因是 Key 没读到。检查TAOTOKEN_API_KEY环境变量是否导出(echo $TAOTOKEN_API_KEY),以及config.toml里的api_key_env名字是否和实际环境变量一致。403 则通常是权限问题,检查required_permission是否在permissions.default_user里。

5.2 404:路径拼接错误

如果 base URL 写成了带/v1或带查询串的形式,SDK 再拼一次/chat/completions就会 404。统一写https://taotoken.net/api,让 SDK 自己补路径。

5.3 模型不调用工具,只回自然语言

三个排查方向:一是tool_choice被设成none;二是工具description太模糊,模型判断不出该不该用;三是用户 query 本身不需要工具,比如「你好」这种。可以先用一句明确需要外部数据的 query 测试,比如「查一下工单 TK-123456」。

5.4 参数校验失败:missing required property

模型生成的 JSON 缺了必填字段。解决方式是在参数description里写清楚格式和示例,并在 Harness 层做一次 Pydantic 校验,校验失败时把错误信息作为tool消息回传给模型,让它重新生成。不要直接抛异常中断,那样用户体验很差。

5.5 工具调用死循环

模型反复调用同一个工具、拿到相同结果还不罢休。max_tool_rounds是第一道防线;第二道是在 Harness 里记录「同一工具+同一参数」的调用次数,超过 2 次就中断并返回提示。日志里如果看到连续多条相同tool_call,基本就是这个问题。

5.6 超时与重试

外部系统慢的时候,timeout_seconds设太小会频繁失败,设太大又拖垮整体响应。经验值是 3~10 秒,配合 2 次指数退避重试。重试仍失败时,把错误信息回传给模型,让它决定是换工具还是告知用户,而不是直接 500。

6. 下一步:把 Harness 接到你的真实系统

跑通上面这条链路后,你已经有了一个能操作外部系统的最小 Agent。接下来可以做的几件事:

把 mock 服务换成你真实的业务接口,注意在 Harness 层加输入过滤,防止模型生成的参数被拼进 SQL 或命令里。工具数量超过 10 个之后,考虑用向量检索先筛出 Top 5 相关工具再传给模型,能明显提升选择准确率并省上下文。多轮调用场景下,把每轮的tool_call_id和结果都存下来,方便回溯。

如果你要长期跑编码类或 Agent 类任务,可以了解下 Coding Plan,按量计费更适合高频调用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite

想先在网页上验证模型对某个工具 Schema 的理解是否准确,用模型对话快速试几轮最省事:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite

接入过程中遇到字段对不上、返回结构不一致的问题,直接翻接入文档,里面有各接口的完整字段说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_campaign=rewrite

最后一句实在话:Function Calling 的坑大多不在模型,而在你的工具描述和参数 Schema 写得够不够清楚。把description当成写给一个聪明但完全不了解你系统的同事看,准确率会肉眼可见地涨。

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

Multi-bit触发器MBFF全流程优化:从时钟功耗到布局实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 4:25:34

Claude Code 使用手册:CLI 配置与 Slash Commands 实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 4:23:53

SPWM三种调制方式详解:从原理到Simulink仿真性能对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华