不是再包一层 Tool Calling,而是把异步真正做进 Agent Runtime:loopa

不是再包一层 Tool Calling,而是把异步真正做进 Agent Runtime:loopa

如果你做过一点 Agent,很快就会遇到一个尴尬的现实:

很多项目都能演示"模型调用一个工具,然后返回答案",但一旦工具开始变复杂,尤其是出现异步任务时,整套设计就开始失真。

问题通常会从这里开始:

  • 一个回合里不止一个工具调用
  • 有些工具秒回,有些工具要跑几秒甚至更久
  • 有些工具在本地执行,有些工具来自远端 MCP server
  • MCP 的异步任务需要轮询状态
  • 如果让模型自己轮询,会不断消耗 token

loopa 就是围绕这些问题写的一个 Go Agent Runtime。

它不是一个"把 OpenAI function calling 再封装一层"的 demo,而是把 本地工具、MCP 工具、同步调用、异步调用、会话摘要 放进同一个运行时里,重点解决一个核心问题:

异步工具到底应该怎么进入 Agent Runtime,而不是只停留在概念上。

项目地址:

  • GitHub: https://github.com/minhyannv/loopa

为什么"异步"才是真正的分水岭

同步工具并不难理解:

  • 模型说要调工具
  • runtime 执行工具
  • 立刻拿到结果
  • 再把结果回给模型

这条链路很直。

但异步工具一出现,事情就完全变了。

比如这些场景:

  • 本地端口扫描
  • 远端排队任务
  • 需要持续几轮才能完成的 MCP tool
  • 需要取消、超时、轮询状态的长任务

这时你就必须回答几个工程问题:

  1. 异步工具是否要暴露成另一套接口?
  2. 一个回合里多个异步工具怎么并发启动?
  3. 工具结果返回给模型时,顺序怎么保证?
  4. MCP Tasks 的轮询由谁负责?
  5. 超时或取消时,是谁去通知远端任务结束?

很多"Agent 示例"不会真的碰到这些问题,所以看起来简单。一旦要做成真正能用的 runtime,这几件事就绕不过去。

loopa 的核心思路

loopa 的设计很直接:

  • 模型只负责决策
    模型决定是否调用工具、调用哪个工具
  • runtime 负责执行
    runtime 负责同步调用、异步调度、MCP 轮询、取消、摘要
  • 本地工具和 MCP 工具统一接入
    最终都变成统一的 tool surface
  • 同步和异步统一接入
    loop 不需要知道"这是本地还是远端",只需要知道"它是 sync 还是 async"

这四件事放在一起,项目的边界就清楚了:

  • 模型做语义层决策
  • 运行时做调度层执行

真正麻烦的那部分不是"让模型调工具",而是"让运行时把异步工具跑完整"。

这个项目最值得看的地方:本地异步和 MCP 异步是统一的

很多系统会把这两类异步拆成两套逻辑:

  • 本地异步工具一套
  • MCP 异步任务一套

loopa 不是这么做的。

它的做法是:

  • 本地异步工具实现 AsyncTool
  • MCP 上需要走 task 的工具,也被适配成 AsyncTool
  • AgentLoop 只认 Execute / ExecuteAsync

这意味着对 loop 来说:

  • port_scan 是异步工具
  • mcp__remote__slow_echo 也是异步工具
  • 它不需要知道这两者底层一个是本地 goroutine,一个是 MCP Tasks

这就是这个项目最核心的工程价值:

异步不是某一种 transport 的特性,而是 runtime 的一级抽象。

异步工具在 loopa 里是怎么跑起来的

第一步:模型返回 tool calls

loopa 仍然基于 chat.completions 做模型调用。

每轮里,模型拿到:

  • 当前消息列表
  • 当前可用工具 schema

然后返回:

  • 普通文本
  • 或者 tool calls

如果返回的是 tool calls,AgentLoop 就进入工具执行阶段。

第二步:同步工具立即执行,异步工具先启动

这一段是整个 runtime 的关键。

AgentLoop 的策略不是"拿到一个工具调一个工具",而是:

  1. 遍历本轮所有 tool calls
  2. 同步工具立即执行
  3. 异步工具先全部启动
  4. 再按原始顺序等待异步结果

这么做有两个好处:

  • 异步工具能真正并发起来
  • 回填给模型的 tool message 顺序仍然稳定

这个点非常重要,因为大模型上下文是顺序敏感的。

如果结果顺序飘来飘去,模型行为也会变得不稳定。

第三步:把工具结果统一回填给模型

无论工具是:

  • 本地同步
  • 本地异步
  • MCP 同步
  • MCP 异步

最后都会被转换成普通的 tool result message,重新送回下一轮模型请求。

所以从模型视角看,世界很简单:

  • 它提出一次工具调用
  • 稍后收到一条工具结果

而中间那一整套同步 / 异步 / 轮询 / 取消 / transport 差异,全部由 runtime 吃掉。

MCP 异步为什么不能交给模型轮询

这是 loopa 里我最想强调的一点。

很多人第一次做 MCP Tasks,会自然想到:

"既然有任务状态,那就让模型继续调用一个查询状态的工具,不就行了吗?"

问题是这样做会把调度逻辑塞回模型:

  • 模型要反复参与状态查询
  • 每轮查询都会增加 token 成本
  • 模型会被迫理解"不属于业务语义"的轮询流程
  • 一旦状态多、轮询久,成本和复杂度都会上升

loopa 的做法正好相反:

  • 模型只发起一次异步工具调用
  • 宿主在后台做 tasks/get
  • 到终态后由宿主取 tasks/result
  • 最后只把最终结果返回给模型

这意味着 MCP 异步在这里不是"多轮模型交互",而是"单次模型调用 + 宿主后台完成任务管理"。

这是一个非常重要的边界划分。

MCP task 流程图

渲染错误: Mermaid 渲染失败: Parse error on line 7: ...Server Model->>Loop: tool call ---------------------^ Expecting '+', '-', 'ACTOR', got 'loop'

注意这里真正负责轮询的是 Client,不是模型。

loopa 是怎么判断一个 MCP 工具该同步还是异步的

这个项目还有一个比较工程化的点:

它不是在执行时才临时决定工具类型,而是在加载阶段就定下来。

具体判断依据有两层:

  1. MCP server 是否声明了 tasks.requests.tools.call
  2. 每个工具自己的 execution.taskSupport

然后在加载阶段直接完成映射:

  • required -> 异步工具
  • optional -> 如果 server 支持 task,就按异步工具处理
  • 缺省 / forbidden -> 同步工具

这样做的好处是:

  • 注册后的工具语义明确
  • AgentLoop 不需要在执行时再临时猜路径
  • 本地工具和 MCP 工具统一暴露成相同接口

技术结构为什么是现在这样

如果用一句话概括:

  • main.go 负责装配
  • agent/ 负责控制流
  • tools/ 负责统一工具接口
  • mcp/ 负责远端工具接入和 task 轮询

总体结构

User Input
RunInteractive
AgentLoop
Session
OpenAIChoiceCreator
chat.completions
Tool Registry
Local Tools
MCP Loader + Clients
stdio
Streamable HTTP

各模块职责

  • agent/loop.go
    管理模型与工具之间的迭代执行
  • agent/session.go
    管理消息历史、摘要状态和 system prompt 重建
  • tools/registry.go
    统一注册和分发工具
  • mcp/loader.go
    发现 MCP 工具,并决定它们是 sync 还是 async
  • mcp/task.go
    负责异步 task 的轮询与结果获取

这个拆法的关键在于:

异步逻辑不写在一堆分散的 if/else 里,而是被收敛到明确的运行时边界。

自动摘要为什么在这个项目里也很重要

如果一个 runtime 真要连续交互,除了异步,另一个必须面对的问题就是上下文膨胀。

loopa 采用了比较务实的策略:

  • 超过阈值就摘要
  • 老消息被压缩
  • 最近几轮真实用户消息保留原样
  • system prompt 始终根据最新 summary 重建

这跟异步能力其实是同一类问题:

  • 都不是"模型本身的能力"
  • 都是 runtime 层必须接住的工程问题

所以这个项目真正做的事情,不只是把工具接上,而是把 Agent 在长期运行时一定会遇到的问题 放进了一个统一设计里。

这个项目的工程取舍

1. 不上重型框架

这个项目没有依赖复杂的 agent framework。

不是因为框架不好,而是因为这里更想把 runtime 的关键边界写清楚:

  • 模型做什么
  • runtime 做什么
  • 异步由谁调度
  • 轮询由谁负责

如果这些边界一开始就是黑盒,很难真正理解 Agent Runtime 到底在解决什么问题。

2. 继续使用 chat.completions

虽然现在也有 Responses API、background mode 这些路线,但 loopa 目前仍然围绕 chat.completions 构建。

这样做的好处是:

  • 更容易接 OpenAI-compatible provider
  • 更容易理解 runtime 自己承担了哪些控制逻辑

代价也很明确:

  • tool 调度要自己写
  • MCP task 轮询要自己写
  • 摘要触发与消息重建也要自己写

但对一个"想把运行时做明白"的项目来说,这反而是优点。

3. debug 日志不是附属品

异步一多,没有日志几乎没法排查。

所以 loopa 会在 debug 级别输出这些信息:

  • OpenAI 请求摘要
  • 每轮 loop 的迭代信息
  • 工具参数与结果摘要
  • MCP 连接、初始化、工具发现
  • task 创建、轮询状态、最终结果、取消
  • 自动摘要触发原因

一旦异步出问题,你最想知道的永远是:

  • 卡在模型侧了吗?
  • 卡在工具启动了吗?
  • 卡在 task 轮询了吗?
  • 卡在结果回填了吗?

这些问题如果没有日志,runtime 基本不可维护。

怎么验证它真的把异步做进去了

这也是这个项目比较完整的地方。它不是只有单元测试,而是把异步路径分层测了出来。

第一层:普通自动化测试

bash 复制代码
go test ./...

第二层:工具链路测试

覆盖四种组合:

  • 本地同步工具
  • 本地异步工具
  • MCP 同步工具
  • MCP 异步工具

也就是说,项目不是只测"能不能调一个工具",而是明确覆盖:

本地 / MCP × 同步 / 异步

第三层:真实模型 E2E

通过:

bash 复制代码
RUN_LIVE_MODEL_E2E=1 go test ./e2e -v

可以让真实模型参与整条链路:

  • 模型识别语义并选择工具
  • runtime 执行同步或异步工具
  • MCP task 在宿主侧轮询完成
  • 最终结果回填给模型

这让"异步是不是只是纸面设计"这个问题,有了真正的端到端验证。

这个项目适合谁看

我觉得它特别适合下面几类人:

  • 想自己写 Go Agent Runtime 的人
  • 想把 MCP 真正接到本地运行时里的人
  • 想研究同步 / 异步工具统一抽象的人
  • 想理解"模型调度"和"宿主调度"边界的人

如果你追求的是"直接拿来就能用的全家桶框架",它未必是那个方向。

但如果你想认真理解:

  • 为什么异步工具会改变 runtime 设计
  • 为什么 MCP Tasks 不应该交给模型轮询
  • 为什么运行时必须显式管理上下文和日志

那这个项目就很值得看。

结语

loopa 最有价值的地方,不是"它也支持 tool calling",而是它把 异步 放到了设计中心。

它回答的不是一个 API 问题,而是一个 runtime 问题:

  • 本地异步怎么做
  • MCP 异步怎么做
  • 两者怎么统一
  • 谁来轮询
  • 谁来取消
  • 结果怎么稳定地回填给模型

如果你真的想把 Agent 从 demo 推到工程实现,这几个问题迟早都要面对。

loopa 的价值,就在于它已经把这些问题落成了一套可以跑、可以测、可以继续扩展的 Go Runtime。

项目地址:

  • GitHub: https://github.com/minhyannv/loopa
相关推荐
泉城老铁2 小时前
一分钟搞定SpringBoot+Vue3 整合 SSE 实现实时消息推送
前端·vue.js·后端
6+h2 小时前
【Spring】深度剖析AOP
java·后端·spring
老迟聊架构2 小时前
完全基于对象存储的数据库引擎:SlateDB
数据库·后端·架构
MinterFusion2 小时前
何谓Qt —— 一款跨平台桌面应用开发神器
开发语言·qt·明德融创
小杍随笔2 小时前
【Rust可见性控制:pub、pub(crate)、pub(super)实战】
开发语言·后端·rust
阿蒙Amon2 小时前
C#常用类库-详解CsvHelper
开发语言·数据库·c#
刚入坑的新人编程2 小时前
C++qt(3)-按钮类控件
开发语言·c++·qt
开始了码2 小时前
基于 Qt 实现多客户端 TCP 通信聊天室
开发语言·数据库·php