最近做的企微二开,业务方提了个需求:销售在企微里发"给张三打 VIP 标签",机器人直接调接口打完标签回"已打"。之前的机器人是问答型,问什么答什么,现在要的是执行型——员工一句话,机器人解析意图、挑出对应接口、传参、调用、把结果发回来。把这套链路落下来记一下踩的坑。

底层用的是 Eyun 平台开放的企微 API,统一 POST+JSON,鉴权用 App Token 加 appid,请求头带 Authorization: Bearer eyk_xxxx,路径统一 {BASE_URL}/wx-api/api/<模块>/<动作>,响应封套 {code, data, detail, message, time},code 为 0 成功。让机器人执行任务,本质是把企微接口包装成大模型可调用的工具,AI 负责解析意图和挑工具,接口负责执行。

第一步:把企微接口注册成工具

大模型 Function Calling 不能直接调企微接口,要先把接口包装成"工具描述"。每个工具描述包含名字、说明、参数 schema。我们把常用的几类接口都注册成工具:

TOOLS = [
    {
        "name": "tag_customer",
        "description": "给客户打标签。需要客户手机号或姓名 + 标签名。",
        "parameters": {
            "type": "object",
            "properties": {
                "keyword": {"type": "string", "description": "客户手机号或姓名"},
                "labels": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["keyword", "labels"]
        }
    },
    {
        "name": "search_customer",
        "description": "按手机号或姓名搜客户档案",
        "parameters": {
            "type": "object",
            "properties": {"keyword": {"type": "string"}},
            "required": ["keyword"]
        }
    }
]

工具描述越具体,模型挑工具越准。早期写得太笼统("执行业务操作"),模型经常挑错——比如把"查客户"误挑成 tag_customer。

第二步:消息进来——Webhook 收指令

员工在企微发"给张三打 VIP 标签",消息通过 Webhook 进来。回调里带 fromUin、content、appid。把消息原样喂给大模型,让模型判断要不要调工具:

@app.route("/wx-api/webhook/", methods=["POST"])
def webhook():
    if request.headers.get("X-Eyun-Event") != "message":
        return "ok"
    data = request.json["data"]
    messages = [
        {"role": "system", "content": "你是企业微信任务机器人,按用户指令调用工具。"},
        {"role": "user", "content": data["content"]}
    ]
    call_llm_with_tools(data["appid"], data["fromUin"], messages)
    return "ok"

Webhook 路径 /wx-api/webhook/,首次创建返回 secret 用于验签。回调至少一次投递,下游处理要幂等,靠 delivery ID 去重——员工连发两次同样指令不能打两次标签。

第三步:参数提取和实体消解

模型决定调 tag_customer 后,要传"张三"和 ["VIP"] 这两个参数。但"张三"是名字不是 uin,标签接口要的是 uin。这步要补一个工具链:先调搜索接口拿 uin,再调标签接口。

落地时不要让模型一步到位,让它分多步调:

def call_llm_with_tools(appid, from_uin, messages):
    resp = requests.post(LLM_URL, json={
        "model": "gpt-4",
        "messages": messages,
        "tools": TOOLS,
        "tool_choice": "auto"
    }).json()
    msg = resp["choices"][0]["message"]
    if msg.get("tool_calls"):
        for call in msg["tool_calls"]:
            result = dispatch_tool(appid, call["name"], json.loads(call["arguments"]))
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": json.dumps(result)})
        # 把工具结果回喂给模型,让它决定下一步或回答
        return call_llm_with_tools(appid, from_uin, messages)
    # 模型不再调工具,把最终回答发回企微
    send_text(appid, from_uin, msg["content"])

模型自己决定调几个工具、按什么顺序调,开发只负责 dispatch_tool 的具体实现。这一层多轮调用是把"AI 编排"和"接口执行"分开的关键。

第四步:工具执行——调真正的企微接口

dispatch_tool 是工具描述名到企微接口的映射层。tag_customer 工具内部要先调 contact/search 拿 uin,再调 label/updateLabel:

def tag_customer(appid, keyword, labels):
    # 先搜客户拿 uin
    search_resp = requests.post(
        f"{BASE}/wx-api/api/contact/search",
        headers=HEADERS,
        json={"appid": appid, "keyword": keyword}
    )
    customers = search_resp.json()["data"].get("list", [])
    if not customers:
        return {"success": False, "message": f"没搜到客户:{keyword}"}
    if len(customers) > 1:
        return {"success": False, "message": f"搜到多个客户,请补手机号精确定位"}
    uin = customers[0]["uin"]
    # 调标签接口
    resp = requests.post(
        f"{BASE}/wx-api/api/label/updateLabel",
        headers=HEADERS,
        json={"appid": appid, "uin": uin, "labels": labels}
    )
    body = resp.json()
    if body["code"] == 0:
        return {"success": True, "message": f"已给 {keyword} 打标签 {labels}"}
    return {"success": False, "message": body["message"]}

工具内部要处理一类常见错误:搜到多个客户不能瞎选第一个,让模型回问员工补信息;接口返回 code 不是 0 时把 message 原样回给模型,模型会自然把它转成自然语言告诉员工。

第五步:权限校验是硬边界

不是所有员工都能让机器人执行所有工具。执行前按员工角色查权限:

def check_tool_permission(appid, from_uin, tool_name):
    profile = requests.post(
        f"{BASE}/wx-api/api/contact/getUserProfileDetail",
        headers=HEADERS,
        json={"appid": appid, "uin": from_uin}
    ).json()["data"]
    return tool_name in role_to_tools(profile.get("roles", []))

无权限不调接口,直接让模型回"您没有权限使用此操作"。权限白名单按岗位配,不按个人——运营调整岗位定义后所有该岗位员工权限实时生效。这一层不做严,员工用机器人绕过审批直接给客户打"已退款"标签是早晚的事。

第六步:执行留痕

机器人执行类操作要留审计日志,事后追溯"谁让机器人给谁打了什么标签"。日志结构记三件事:触发指令(员工原文)、工具调用链(调了哪些工具、传了什么参、返回什么)、最终回发给员工的内容。审计日志和接口调用日志分开存——接口调用日志用于排障,审计日志用于合规。

几个落地要点

工具描述要写细:每个工具的 description 里写清"什么场景用、什么场景不用",模型挑工具的准确率从笼统描述的 70% 提到 90% 以上。

工具粒度别太细:最早把"搜客户""查客户档案""打标签"拆成三个工具让模型自己编排,结果模型经常跳过搜直接调标签接口报错。后来把"搜客户 + 打标签"合并成 tag_customer 一个工具,内部按步骤调多个接口,模型只管调一次。

兜底分支不能少:模型置信度低时不要硬调工具,让它回问员工"您说的张三是指哪个张三"。早期没做兜底,模型瞎挑客户打错标签,花了一周善后。

写在最后

让机器人执行任务这套,本质是把企微接口包装成工具,让大模型负责意图解析和编排、接口负责执行、消息接口负责把结果发回去。AI 在这套链路里是个"调度员",真正干活的是接口。把工具描述写细、权限边界守严、兜底分支留够,机器人才能从"问答型"升级成"执行型"——员工一句话办成一件事,不是和机器人聊半天。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐