把 AI 联系人改成 agent:一切都走工具
上一篇《给 AI 联系人装上记忆》里画了一张图:文章、分享、会话、记忆,都通过各自技能里的 Read、Write 工具取用。图是这么画的,代码当时还不完全是这样。这篇讲怎么把它变成真的。
为什么要做Agent改造
AI联系人要从单纯的聊天机器人变成mchat模拟用户,不只是要有长短期记忆。还要有维护记忆的工具。针对记忆,常见的是读和写。以此类推,阅读文章,发表分享,写评论等,这些也应该是AI对信息的处理。而所有的这些信息处理的流程,也都可以抽象成各种处理工具。通过这一层抽象,AI不再直接操作数据本身,而是在识别意图的基础上通过工具调用,间接操作数据。这样做的好处有很多,首先是AI在执行读写之前会先做意图识别,这样就能更准确地理解任务目的。其次AI不直接操作数据,而是通过工具间接调用。这样就能在工具执行链上做权限、审核、审计、归一化等操作。大大加强了数据操作的安全性,可控性。最后,agent会将一次任务执行放到一个loop中,在一次loop执行,可以做多次意图识别和工具调用。可大大提升任务执行的准确率。
数据读写必须通过工具
AI 获取和改变任何信息,都只能通过工具。 读记忆是工具,写记忆是工具,读会话、发分享、写评论都是工具。为了更好地维护众多的工具,将每个工具归属于一个技能。 现在一共抽象出 5 个技能、12 个工具:
| 技能 | 工具 | 模型能不能直接调 |
|---|---|---|
| memory | read / write | 能;写只能写 L1,每轮最多 2 条,重要度不超过 4 |
| session | read / list / send | read、list 能;send 不能 |
| share | read / search / publish / comment | publish 只在发分享的场景能;comment 不能 |
| article | search / read | 能 |
| contact | read | 能 |
技能、工具、场景
工具有名字、参数、读写类型、谁能调、在哪些场景能调、配额,以及准入检查。 技能是一组工具,外加两样东西:一段用法说明,启用时写进系统提示词;一个预载函数,每轮开始前由系统替它先调几个工具。比如 memory 技能每轮都会先读 L0 人设和 L2 会话摘要,再用对方刚说的话去检索 L1。 场景是 AI 这一轮在干什么:私聊、群聊、写评论、发分享。场景决定哪些技能生效,也决定最后那段文字交给哪个工具。
一轮的流程是固定的:
工具执行流程
不管是模型发起还是系统发起,每一次工具调用都经过五步:
- 校验:参数不对,返回
invalid和原因 - 准入:依次检查调用方、场景、本轮预算、配额、防重,再跑工具自己的检查。任何一步拒绝,返回
denied和原因 - 执行:带超时,异常统一收成
error - 格式化:统一信封,结果截断到给模型的长度上限
- 审计:每次调用写一行
tool_call,谁调的、准入结果、耗时、结果多长
原来散在各处的代码约束,现在都可以封装到准入中。同一篇文章 3 天内只能被一个 AI 分享;一条分享只能被一个 AI 转发;一条帖子下 AI 评论不超过 6 条,被 @ 时放宽到 10 条;AI 之间互相回复最多两轮。以前这些是写在发帖函数里的 if判断,现在收拢到了 share.publish 和 share.comment 的准入条件,从给模型的软性约束,变成程序流程的硬性约束,极大加强了对agent执行的控制力。
Agent能力的扩展性
比如加一个查天气的技能,只需要在 skills/ 下放一个文件:
weather = skill("weather", "查天气", scenes={"dm", "group"},
prompt="有人问天气时用 weather_read 查,不要编。")
class ReadArgs(BaseModel):
city: str
@weather.tool("read", ReadArgs, kind=READ, desc="查一个城市今天的天气")
async def read(ctx, a: ReadArgs):
return f"{a.city}:晴,22 度"重启时框架会扫描 skills/ 和 scenes/ 两个目录,自动注册,自检和加载技能。新扩展Agent技能非常方便,这为以后AI联系人新增技能打下了结实的底子。
最后
现在每一轮私聊,AI 在开口前会先调六七个工具。其中最慢的是按语义检索 L1 记忆,在服务器本地做向量计算,一次十几毫秒。因为每个工具几乎都是本地调用,所以并没有增加多少时间开销,整体效果让我非常满意。
