1. 项目缘起:当AI Agent需要“接地气”时,我们聊什么

最近在折腾AI Agent,一个绕不开的话题就是:怎么让它从“云端”走下来,真正触达用户?很多开发者把Agent的逻辑和对话能力打磨得相当不错,但最后一步——如何让用户方便地与之交互——却成了拦路虎。直接让用户去访问一个网页或打开一个命令行工具,体验割裂,用户留存率也低。于是,将Agent接入到用户最高频的通讯工具里,就成了一个刚需。

在国内,这个最高频的工具,毫无疑问是微信。无论是个人微信的私聊、群聊,还是企业微信的工作流,都是绝佳的交互入口。但微信生态的封闭性众所周知,官方API门槛高、审核严,对于快速迭代、功能灵活的Agent项目来说,直接对接并不友好。这时候,社区的力量就显现出来了,各种基于逆向工程或协议模拟的“微信机器人”框架层出不穷,iLink Bot API就是其中在功能和稳定性上口碑不错的一个。

另一方面,Agent的能力构建也在模块化。像“Skill”这样的概念,就是把特定任务(比如查天气、订机票、控制智能家居)封装成可插拔的模块。网上能找到不少开源的Skill实现,这为我们快速赋予Agent实用能力提供了可能。

所以,这个项目的核心目标就很清晰了: 利用Codex作为Agent的核心“大脑”,通过iLink Bot API打通微信这个“手脚”,再集成一个现成的开源Skill作为“专业技能”,打造一个能跑在微信里、具备特定实用功能的智能助手。 这不仅仅是技术拼接,更是一次关于如何让AI应用“落地”的完整实践。接下来,我会手把手带你走通全流程,并分享其中几个关键环节的“坑”与“解”。

2. 技术栈选型与核心组件拆解

在动手之前,我们必须对用到的几个核心组件有清晰的认识,理解它们各自扮演的角色以及为什么选它们。

2.1 Codex:不止是代码生成,更是推理引擎

很多人对Codex的第一印象是“那个能写代码的AI”。没错,但在这个项目里,我们看中的是它作为 通用文本推理和任务规划引擎 的能力。相比于专门针对对话优化的模型,Codex在理解复杂指令、进行逻辑分解、调用工具(Skill)方面表现出更强的结构化能力。

注意:这里提到的“Codex”是一个泛指,在实际部署中,它可能指向OpenAI的 code-davinci-002 等模型,也可能是其他具备类似代码/推理能力的大语言模型API。本项目的核心思想是架构,具体模型后端可以替换。

为什么是Codex而不是纯聊天模型?

  1. 工具调用范式 :我们需要Agent能理解“请使用【天气查询Skill】看看北京明天天气”这样的指令,并准确解析出意图(查询天气)和参数(北京、明天),然后调用对应的Skill。Codex在代码生成中训练的“函数调用”思维模式,与此高度契合。
  2. 状态保持与规划 :一个复杂的用户请求可能涉及多步操作。Codex能够更好地维持对话上下文,并规划步骤序列,例如“先查机票,再查酒店,最后对比一下”。
  3. 与Skill的天然亲和 :很多Skill本身就是一段代码或一个API。用Codex来理解和生成调用这些Skill的指令,更加自然。

在实际部署中,你需要一个能访问Codex系列模型(或类似能力模型)的API密钥和端点。这可能是OpenAI的官方API,也可能是部署在本地或私有云上的开源模型。

2.2 iLink Bot API:非官方但稳定的微信连接器

微信官方没有提供用于开发个人聊天机器人的API。iLink Bot API这类方案,本质上是通过模拟微信Web端或PC客户端的协议,实现自动化收发消息。选择iLink Bot API,我主要基于以下几点考虑:

  1. 协议层封装 :它封装了微信登录、心跳维持、消息监听、消息发送等底层复杂且易变的协议细节,提供了相对稳定的HTTP或WebSocket接口供开发者调用。这意味着我们不需要关心微信协议的具体实现和频繁变更。
  2. 功能完整性 :支持文本、图片、语音、文件、名片、链接等多种消息类型的收发,能满足Agent丰富的交互需求。
  3. 活跃的社区与更新 :这类项目最大的风险是微信客户端升级导致协议失效。一个活跃的项目能较快地跟进修复,iLink在这方面口碑较好。
  4. 部署相对简单 :通常提供Docker镜像或可执行文件,降低了环境配置的复杂度。

重要提醒 :使用此类非官方API存在一定风险,包括但不限于账号被限制功能或封禁。务必用于学习、测试或合规场景,避免高频、营销式消息推送。在实际项目中,建议使用企业微信机器人等官方合规方案作为生产环境首选,本方案更适合技术探索和原型验证。

2.3 开源Skill:快速赋予Agent“超能力”

Skill是Agent的能力单元。一个典型的Skill通常包括:

  • 技能描述 :用自然语言描述这个Skill能做什么。
  • 触发关键词/意图 :定义哪些用户输入会触发此Skill。
  • 参数解析 :从用户输入中提取Skill所需的参数。
  • 执行函数 :调用外部API或执行本地逻辑来完成任务的代码。
  • 响应格式化 :将执行结果组织成友好的文本回复。

网上有大量开源Skill,例如:

  • 天气查询Skill :调用和风天气等API。
  • 新闻摘要Skill :抓取并总结指定网站的新闻。
  • 笔记管理Skill :连接Notion或Obsidian的API进行增删改查。
  • 计算器Skill :进行复杂数学运算或单位换算。

在本项目中,我们假设你已经找到了一个心仪的开源Skill,例如一个“待办事项管理Skill”。我们的任务就是将它集成到Codex Agent中。

3. 系统架构设计与数据流剖析

理解了各个组件后,我们需要设计一个清晰的架构,让它们协同工作。下图展示了核心的数据流(注:此处用文字描述架构,不使用Mermaid图表):

整个系统可以看作一个事件驱动的管道:

  1. 消息入口 :用户在你的微信上(可能是你专门用于测试的微信号)发送一条消息。
  2. iLink Bot 捕获 :iLink Bot程序(通常运行在一台服务器或你的电脑上)登录了你的微信,并监听到这条消息。
  3. HTTP Webhook 转发 :iLink Bot将收到的消息(包含发送者、内容、类型等信息)通过预先配置的Webhook URL,以HTTP POST请求的形式推送到我们的 Agent中枢服务
  4. Agent中枢服务(核心) :这是一个我们自建的Web服务(可以用Python Flask/FastAPI, Node.js Express等实现),它是整个系统的大脑。
    • 请求路由 :接收来自iLink的Webhook。
    • 上下文管理 :为每个微信用户(或群聊)维护一个对话历史记录。
    • Codex调用 :将当前用户消息和对话历史组合成一个精心设计的Prompt,发送给Codex API。这个Prompt的职责是:分析用户意图,判断是否需要调用Skill,以及如何调用。
    • Skill调度 :如果Codex的判断结果是需要调用某个Skill(例如,“调用TodoSkill,动作为‘添加’,内容为‘下午三点开会’”),中枢服务就找到对应的Skill执行函数,传入参数并执行。
    • 响应生成 :将Skill的执行结果(或Codex直接生成的对话回复)整理成格式化的文本(或图文)。
  5. HTTP 回调 :中枢服务将最终要回复的内容,通过HTTP请求发送回iLink Bot API提供的消息发送接口。
  6. 消息出口 :iLink Bot API模拟微信客户端,将回复消息发送到对应的微信聊天窗口。

这个架构的关键在于 Agent中枢服务 ,它实现了Codex与Skill、Codex与微信之间的“翻译”和“调度”工作。

4. 实战部署:从零搭建你的微信AI助手

理论清晰后,我们进入实战环节。我会以Python技术栈为例,分步讲解。

4.1 环境准备与依赖安装

首先,确保你的开发环境已就绪。

# 创建一个新的项目目录
mkdir wechat-agent-assistant && cd wechat-agent-assistant

# 创建虚拟环境(推荐)
python -m venv venv
# 激活虚拟环境
# Windows: venv\Scripts\activate
# Mac/Linux: source venv/bin/activate

# 安装核心依赖
pip install fastapi uvicorn httpx pydantic openai
# fastapi: 用于构建Agent中枢Web服务
# uvicorn: ASGI服务器,用于运行FastAPI应用
# httpx: 现代化的HTTP客户端,用于调用Codex API和iLink Bot API
# pydantic: 用于数据验证和设置管理
# openai: OpenAI官方库,方便调用Codex(如果使用OpenAI的话)

此外,你还需要准备:

  • 一个用于测试的微信账号。
  • iLink Bot API的可执行文件或Docker镜像(从其官方仓库获取)。
  • 一个开源Skill的代码(例如,从GitHub上clone一个Todo Skill)。
  • 你的Codex API密钥和基础URL(如果是OpenAI,则只需API Key)。

4.2 配置并启动iLink Bot

这一步的目标是让iLink Bot登录微信并准备好接收/发送消息。

  1. 获取iLink Bot :从iLink项目的Release页面下载对应你操作系统的客户端。
  2. 配置文件 :通常需要一个 config.yaml config.json 。关键配置如下:
    # 示例 config.yaml
    bot:
      # Webhook地址,指向我们即将启动的Agent中枢服务
      webhook: "http://localhost:8000/ilink/webhook"
      # 其他配置如日志级别等
      log_level: "info"
    
    # 微信协议相关配置(具体字段请参考iLink文档)
    wechat:
      # 可能包含登录设备类型、缓存路径等
      device: "pad"
    
  3. 运行iLink Bot
    # 假设可执行文件名为 ilink-bot
    ./ilink-bot --config ./config.yaml
    
    运行后,程序会输出一个二维码,用你的测试微信扫码登录。登录成功后,iLink Bot就开始运行了。它会监听微信消息,并将收到的任何消息POST到 webhook 配置的地址(本例中为 http://localhost:8000/ilink/webhook )。

4.3 构建Agent中枢服务(FastAPI实现)

这是最核心的编码部分。我们创建一个 main.py 文件。

from fastapi import FastAPI, Request, HTTPException
from pydantic import BaseModel
import httpx
import json
import logging
from typing import Dict, Any, Optional

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="WeChat Agent Hub")

# --- 配置(应从环境变量或配置文件中读取)---
CODEX_API_KEY = "your-codex-api-key"
CODEX_API_BASE = "https://api.openai.com/v1"  # 如果是其他部署,修改此处
CODEX_MODEL = "code-davinci-002"
ILINK_SEND_API = "http://localhost:8080/send"  # iLink Bot的消息发送API地址,需查阅iLink文档

# 模拟一个简单的内存存储,用于维护用户对话上下文。生产环境应使用Redis或数据库。
user_contexts: Dict[str, list] = {}

# --- 数据模型定义 ---
class WechatMessage(BaseModel):
    """定义从iLink Bot接收到的消息格式"""
    msg_id: str
    from_user: str
    to_user: str
    msg_type: int  # 1-文本,3-图片...
    content: str
    # 根据iLink实际推送的字段进行调整

class SkillRequest(BaseModel):
    """定义调用Skill时的请求格式"""
    skill_name: str
    action: str
    parameters: Dict[str, Any]

# --- 导入并初始化你的开源Skill ---
# 假设我们有一个本地文件 `todo_skill.py`,里面有一个 `handle_todo` 函数
try:
    from todo_skill import handle_todo
    AVAILABLE_SKILLS = {
        "todo": handle_todo
    }
except ImportError:
    logger.warning("Todo skill not found. Running without it.")
    AVAILABLE_SKILLS = {}

# --- 核心函数:调用Codex进行意图解析和规划 ---
async def call_codex_for_planning(user_input: str, conversation_history: list) -> Dict[str, Any]:
    """
    将用户输入和对话历史发送给Codex,让它判断是否需要调用Skill,以及如何调用。
    返回Codex的解析结果。
    """
    # 构建Prompt,这是让Codex正确工作的关键!
    prompt = f"""
    你是一个AI助手,可以调用工具(Skill)来帮助用户。
    你拥有的工具(Skill)列表:
    {json.dumps(list(AVAILABLE_SKILLS.keys()), indent=2)}

    对话历史:
    {json.dumps(conversation_history[-5:], indent=2)} # 只保留最近5轮历史

    用户最新输入:{user_input}

    请分析用户意图,并严格按照以下JSON格式回复:
    {{
      "thought": "你的思考过程,解释用户想做什么,是否需要调用Skill。",
      "need_skill": true/false,
      "skill_name": "如果需要调用Skill,这里是Skill的名字,否则为null",
      "action": "Skill需要执行的动作,如'add', 'query', 'delete'",
      "parameters": {{}} // 调用Skill所需的参数键值对
    }}
    如果不需要调用Skill,请直接生成友好回复放在"thought"字段,并设置"need_skill": false。
    """
    headers = {
        "Authorization": f"Bearer {CODEX_API_KEY}",
        "Content-Type": "application/json"
    }
    data = {
        "model": CODEX_MODEL,
        "prompt": prompt,
        "max_tokens": 500,
        "temperature": 0.1, # 低温度保证输出格式稳定
        "stop": ["\n\n"] # 停止符可根据情况调整
    }
    async with httpx.AsyncClient() as client:
        try:
            resp = await client.post(f"{CODEX_API_BASE}/completions", headers=headers, json=data, timeout=30.0)
            resp.raise_for_status()
            result = resp.json()
            text_output = result["choices"][0]["text"].strip()
            # 解析Codex返回的JSON
            logger.info(f"Codex raw output: {text_output}")
            # 这里需要做健壮的JSON解析,Codex有时会在JSON外加引号或说明
            parsed_output = json.loads(text_output)
            return parsed_output
        except (httpx.HTTPError, json.JSONDecodeError) as e:
            logger.error(f"Error calling Codex: {e}")
            return {"thought": "抱歉,我暂时无法处理你的请求。", "need_skill": False}

# --- 核心函数:执行Skill ---
async def execute_skill(skill_name: str, action: str, parameters: Dict) -> str:
    """根据Codex的指示,执行具体的Skill"""
    if skill_name not in AVAILABLE_SKILLS:
        return f"错误:未找到名为 '{skill_name}' 的技能。"
    skill_func = AVAILABLE_SKILLS[skill_name]
    try:
        # 调用Skill函数,传入动作和参数
        result = await skill_func(action, parameters) # 假设skill函数是异步的
        return str(result)
    except Exception as e:
        logger.exception(f"Error executing skill {skill_name}: {e}")
        return f"执行技能 '{skill_name}' 时出错:{str(e)}"

# --- 核心函数:通过iLink Bot发送消息 ---
async def send_wechat_message(to_user: str, content: str):
    """调用iLink Bot的API发送消息回微信"""
    async with httpx.AsyncClient() as client:
        payload = {
            "toUser": to_user,
            "content": content,
            "msgType": 1 # 文本消息
        }
        try:
            resp = await client.post(ILINK_SEND_API, json=payload, timeout=10.0)
            resp.raise_for_status()
            logger.info(f"Message sent to {to_user}")
        except httpx.HTTPError as e:
            logger.error(f"Failed to send message via iLink: {e}")

# --- Webhook 端点:接收微信消息 ---
@app.post("/ilink/webhook")
async def handle_wechat_message(request: Request):
    """处理从iLink Bot转发过来的微信消息"""
    # 1. 解析消息
    msg_data = await request.json()
    # 这里需要根据iLink实际的推送格式来解析,以下为示例
    message = WechatMessage(**msg_data)
    logger.info(f"Received msg from {message.from_user}: {message.content}")

    # 只处理文本消息
    if message.msg_type != 1:
        return {"status": "ignored"}

    user_id = message.from_user
    user_input = message.content.strip()

    # 2. 获取或初始化该用户的对话上下文
    if user_id not in user_contexts:
        user_contexts[user_id] = []
    history = user_contexts[user_id]

    # 3. 将用户输入加入历史
    history.append({"role": "user", "content": user_input})

    # 4. 调用Codex进行意图分析和规划
    codex_response = await call_codex_for_planning(user_input, history)

    final_reply = ""
    # 5. 根据Codex的判断执行相应操作
    if codex_response.get("need_skill", False):
        skill_name = codex_response.get("skill_name")
        action = codex_response.get("action", "")
        params = codex_response.get("parameters", {})
        # 执行Skill
        skill_result = await execute_skill(skill_name, action, params)
        final_reply = f"[技能 {skill_name} 执行结果]\n{skill_result}"
        # 将Skill执行结果也作为AI的“发言”加入历史,保持上下文连贯
        history.append({"role": "assistant", "content": f"我调用了{skill_name}技能,结果是:{skill_result}"})
    else:
        # 不需要调用Skill,直接使用Codex的思考过程作为回复(或可让其生成更友好的回复)
        final_reply = codex_response.get("thought", "我想了想,暂时无法处理这个问题。")
        history.append({"role": "assistant", "content": final_reply})

    # 6. 限制历史记录长度,防止无限增长
    if len(history) > 10:
        user_contexts[user_id] = history[-10:]

    # 7. 将回复发送回微信
    await send_wechat_message(message.from_user, final_reply)

    return {"status": "ok", "reply": final_reply}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

4.4 集成一个开源Skill示例

假设我们集成的Todo Skill ( todo_skill.py ) 非常简单:

# todo_skill.py
# 一个极简的内存Todo List Skill示例
_todo_items = []

async def handle_todo(action: str, parameters: dict) -> str:
    """处理待办事项"""
    global _todo_items
    if action == "add":
        item = parameters.get("item", "")
        if item:
            _todo_items.append(item)
            return f"已添加待办:{item}"
        else:
            return "错误:添加待办需要‘item’参数。"
    elif action == "list":
        if not _todo_items:
            return "当前没有待办事项。"
        return "你的待办事项:\n" + "\n".join(f"{i+1}. {item}" for i, item in enumerate(_todo_items))
    elif action == "delete":
        index = parameters.get("index")
        if index is not None and 1 <= index <= len(_todo_items):
            removed = _todo_items.pop(index-1)
            return f"已删除待办:{removed}"
        else:
            return f"错误:索引无效。请输入1到{len(_todo_items)}之间的数字。"
    else:
        return f"错误:不支持的动作 '{action}'。支持的动作:add, list, delete."

将这个文件放在与 main.py 同级目录下,我们的Agent就具备了Todo管理能力。

4.5 联调测试与运行

  1. 启动Agent中枢服务
    uvicorn main:app --reload --host 0.0.0.0 --port 8000
    
    服务将在 http://localhost:8000 运行。
  2. 确保iLink Bot正在运行 ,并且其 webhook 配置指向 http://你的服务器IP:8000/ilink/webhook 。如果是本地测试,就是 http://localhost:8000/ilink/webhook
  3. 进行测试 :用手机微信向登录了iLink Bot的账号发送消息。
    • 发送:“帮我记一下下午三点开会。”
    • Codex应该能解析出意图,调用 todo skill,动作 add ,参数 {"item": "下午三点开会"}
    • 你将在微信中收到回复:“[技能 todo 执行结果]\n已添加待办:下午三点开会”。
    • 发送:“看看我的待办。”
    • 应收到待办列表。

5. 关键环节的避坑指南与优化建议

走通流程只是第一步,要让这个系统稳定、可用,还需要注意以下这些我踩过的坑。

5.1 Codex Prompt工程:稳定输出的秘诀

Prompt的设计直接决定了Codex能否正确理解任务并格式化输出。上面的示例Prompt只是一个起点,在实际使用中你需要不断优化:

  • 提供清晰的示例(Few-Shot) :在Prompt中加入几个输入输出的例子,能极大提高Codex输出格式的稳定性。例如:
    示例1:
    用户输入:“用todo加一个任务:买牛奶”
    输出:{{"thought": "用户想添加一个待办事项。", "need_skill": true, "skill_name": "todo", "action": "add", "parameters": {{"item": "买牛奶"}}}}
    
    示例2:
    用户输入:“你好吗?”
    输出:{{"thought": "用户在进行日常问候,无需调用技能。", "need_skill": false}}
    
  • 严格限定输出格式 :使用JSON Schema描述或者非常严格的格式说明,并设置较低的 temperature (如0.1)来减少随机性。
  • 处理解析失败 :Codex的输出可能不是完美JSON。代码中必须有健壮的异常处理,比如尝试提取JSON部分,或者准备一个fallback回复。

5.2 iLink Bot的稳定性与消息去重

  • 网络重连 :iLink Bot与微信服务器的长连接可能中断。需要监控其进程,并实现自动重启机制(如使用systemd或supervisor)。
  • 消息去重 :微信协议可能导致消息重复推送。在Webhook处理端,可以根据iLink提供的 msg_id 实现简单的去重逻辑,避免同一指令被处理多次。
  • 异步处理 :微信消息可能并发到达。确保你的Webhook处理逻辑是异步的(如使用 async/await ),并且对于同一个用户的消息处理是串行的(可以用队列),避免上下文错乱。

5.3 Skill的设计与错误处理

  • Skill的标准化接口 :定义清晰的Skill接口(如输入参数、输出格式),便于管理和自动加载。可以使用装饰器或配置文件来注册Skill。
  • 超时与降级 :Skill调用的外部API可能会超时或失败。必须为每个Skill设置超时时间,并提供友好的降级回复(如“服务暂时不可用”)。
  • 权限与安全 :不是所有用户都能调用所有Skill。可以在Skill调度层加入简单的权限校验,例如根据微信用户ID来判断。

5.4 上下文管理的进阶方案

示例中使用内存字典存储上下文,这仅适用于开发和单机测试。生产环境需要考虑:

  • 持久化 :使用Redis或数据库存储对话历史,避免服务重启后失忆。
  • 上下文窗口 :大模型有token限制。需要设计摘要策略,将过长的历史对话总结成一段摘要,再放入Prompt,而不是无脑拼接全部历史。
  • 会话隔离 :清晰区分私聊和群聊上下文,避免信息交叉。

5.5 扩展性思考:如何轻松加入更多Skill

一个优秀的架构应该能轻松扩展。你可以设计一个 Skill 基类,所有Skill都继承并实现 execute 方法。然后使用一个注册表(Registry)来管理所有Skill。在项目启动时,自动扫描某个目录下的所有Skill类并注册。这样,要新增一个Skill,只需要在指定目录下新建一个文件即可,中枢服务无需修改核心代码。

通过以上步骤,你已经成功搭建了一个将Codex智能、iLink连接能力与开源Skill结合起来的微信AI助手原型。这个框架具有很强的可扩展性,你可以不断加入新的Skill(如查快递、算汇率、讲笑话),让它变得越来越强大。记住,核心价值不在于单个技术点多深奥,而在于如何将它们有机组合,解决真实场景下的交互问题。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐