最近做的企微二开,要让大模型不只是聊天,还能真调企微接口办事------客户说"帮我查下张三的订单",大模型理解后自动调企微的客户查询接口和业务系统的订单接口,把结果组织好回给客户。这就是 function calling 范式在企微场景的落地。和之前聊的意图解析层不同,那篇重点在意图识别和槽位,这篇重点在工具怎么注册给大模型、工具调用循环怎么跑、企微接口怎么被包装成工具。把调了哪些接口记下来。
Eyun 平台 开放的企微 API,统一 POST+JSON,鉴权用 App Token 加 appid(Authorization: Bearer eyk_xxxx),路径 {BASE_URL}/wx-api/api/<模块>/<动作>,响应封套 {code, data, detail, message, time},code 为 0 成功。
工具不是接口,是接口的包装
大模型不能直接调企微接口------接口参数复杂、鉴权要带 token、响应格式要解析。要把接口包装成"工具",工具定义清楚名字、描述、参数 schema,大模型按 schema 输出调用参数,我们的执行层拿参数调真接口。
工具注册:把企微接口暴露给大模型
每个企微接口包装成一个工具,注册到工具表:
python
TOOLS = [
{
"name": "query_customer",
"description": "按手机号或昵称查客户信息,返回客户姓名、电话、标签",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "客户手机号或昵称"}
},
"required": ["keyword"]
}
},
{
"name": "send_message",
"description": "给客户发文本消息",
"parameters": {
"type": "object",
"properties": {
"to_uin": {"type": "number", "description": "客户uin"},
"content": {"type": "string", "description": "消息内容"}
},
"required": ["to_uin", "content"]
}
},
{
"name": "update_label",
"description": "给客户打标签",
"parameters": {
"type": "object",
"properties": {
"uin": {"type": "number", "description": "客户uin"},
"labels": {"type": "array", "items": {"type": "string"}, "description": "标签名列表"}
},
"required": ["uin", "labels"]
}
}
]
工具描述要清晰------大模型靠描述判断什么时候调这个工具。描述写模糊,模型调错或不调。
工具执行:参数到接口调用
大模型输出工具名和参数后,执行层拿参数调真企微接口:
python
import requests
BASE = "https://api.eyun.com"
HEADERS = {"Authorization": "Bearer eyk_xxxx", "Content-Type": "application/json"}
def execute_tool(appid, tool_name, args):
if tool_name == "query_customer":
return query_customer(appid, args["keyword"])
elif tool_name == "send_message":
return send_message(appid, args["to_uin"], args["content"])
elif tool_name == "update_label":
return update_label(appid, args["uin"], args["labels"])
else:
return {"error": f"unknown tool: {tool_name}"}
def query_customer(appid, keyword):
if keyword.isdigit() and len(keyword) >= 11:
resp = requests.post(
f"{BASE}/wx-api/api/contact/phoneNumberSearch",
headers=HEADERS,
json={"appid": appid, "phone": keyword}
)
else:
resp = requests.post(
f"{BASE}/wx-api/api/contact/search",
headers=HEADERS,
json={"appid": appid, "keyword": keyword}
)
data = resp.json()["data"]
return {"name": data.get("nickName", ""), "phone": data.get("mobile", "")}
工具执行返回结构化数据,大模型拿数据组织自然语言回复。接口路径和参数在开发文档。
工具调用循环:多轮工具调用
大模型调一个工具拿到结果后,可能还要再调别的工具------查到客户后要查订单,查到订单后要发消息。工具调用循环:
python
def run_conversation(appid, user_msg, max_steps=5):
messages = [{"role": "user", "content": user_msg}]
for step in range(max_steps):
# 1. 大模型决定调什么工具
resp = llm.chat(messages, tools=TOOLS)
# 2. 模型要调工具
if resp["tool_calls"]:
for call in resp["tool_calls"]:
# 3. 执行工具
result = execute_tool(appid, call["name"], call["arguments"])
# 4. 把结果给模型
messages.append({"role": "tool", "tool_call_id": call["id"], "content": str(result)})
else:
# 5. 模型不调工具了,直接回用户
return resp["content"]
return "处理步骤太多,请简化问题"
max_steps 防止死循环------模型反复调工具不收敛。超过步数直接返回提示。不做步数限制,模型可能无限调工具烧 token。
工具权限:不是所有工具都能被调
大模型想调什么就调什么会出事------客户说"帮我删了张三",模型真去调删除接口。工具要按权限分级:
-
只读工具(query_customer、search):模型可自由调
-
写工具(send_message、update_label):要权限校验,且部分要人工确认
-
高危工具(delete、disband):不注册给模型,模型调不到
python
def execute_tool_safe(appid, tool_name, args, caller_role):
tool = TOOL_REGISTRY.get(tool_name)
if not tool:
return {"error": "tool not found"}
# 权限校验
if tool["risk_level"] == "high" and caller_role != "admin":
return {"error": "permission denied"}
# 写操作要确认
if tool["risk_level"] == "medium":
return {"need_confirm": True, "preview": simulate(tool_name, args)}
return execute_tool(appid, tool_name, args)
写操作返回"需要确认",前端给用户确认后再真执行。不做权限分级,模型乱调接口,事故必然。
工具结果回写:结构化数据给模型
工具执行返回的是结构化数据(JSON),模型要基于数据生成自然语言回复。返回给模型时要格式化清楚:
# 工具返回给模型的内容
"工具 query_customer 返回:
客户姓名:张三
手机号:138****8888
标签:高意向、老客户"
结构化数据格式化成文本,模型更容易理解。直接丢 JSON 也行,但模型可能漏读字段。
上下文管理:工具调用历史
多轮工具调用,每轮的调用记录要存上下文,模型能回顾之前调了什么、拿到了什么:
python
messages = [
{"role": "user", "content": "查张三的订单"},
{"role": "assistant", "tool_calls": [{"name": "query_customer", "arguments": {"keyword": "张三"}}]},
{"role": "tool", "content": "客户uin: 8888, 姓名: 张三"},
{"role": "assistant", "tool_calls": [{"name": "query_order", "arguments": {"uin": 8888}}]},
{"role": "tool", "content": "订单SO-123, 金额300元"},
{"role": "assistant", "content": "张三有一笔订单SO-123,金额300元"}
]
上下文让模型知道之前调过什么,不会重复调。不存上下文,模型每轮都从零开始,反复查同一客户。
写在最后
大模型工具接入这套东西,本质是把企微接口包装成工具注册给大模型------phoneNumberSearch、sendText、updateLabel 这些接口变成工具的执行体,大模型按工具 schema 输出参数,执行层调真接口。工具调用循环让模型能多轮调用,权限分级防止乱调。把工具注册对、循环跑稳、权限做严,大模型真能调企微接口办事------而不是只会聊天的摆设。