观察 AIRI 源码:一个 Agent 系统如何处理入口、扩展与执行闭环

在 AI Agent 快速发展的这两年,很多项目都已经能做到"能聊、能演示、能截图。

但真正决定一个项目能走多远的,往往不是首屏效果,而是工程治理能力:请求怎么被接入、会话怎么被隔离、扩展怎么被约束、出错之后怎么继续稳定运行。

Project AIRI 值得借鉴的地方就在这里。 它不是把模型能力包一层 UI,而是在尝试把输入、推理、执行、反馈组织成一套可持续迭代的运行系统。

一、AIRI 是什么:一个"可运行"的 Agent 系统

一句话讲,AIRI 不是"给 LLM 套一层工具调用"的通用 Agent 平台,而是一个面向数字角色场景的运行系统。

它关注的不只是"任务能不能做完",还关注"角色能不能持续存在、持续互动、跨端一致地存在"。 普通 Agent 平台通常把重点放在流程编排:输入任务、调用工具、返回结果。

AIRI 在这条链路之外,多做了三层事情:

  • 实时交互层:语音输入、语音输出、角色驱动(如 Live2D / VRM)要协同工作,体验目标不是一次性响应,而是"在场感"。
  • 多形态运行层:Web、桌面、移动端不是各做一套,而是围绕共享能力组织,确保角色能力跨端延续。
  • 长期运行层 :会话、状态、能力配置、插件扩展都要可持续管理,项目目标是长期演进,不是短期 demo。 所以 AIRI 的核心不是"模型接得多",而是"把模型、交互、执行、扩展放进同一套可运行系统里"。
    这也是它和常见 Agent 框架最容易被混淆、但本质上差异最大的地方。

从仓库结构可以看到它的职责分层:apps 承接入口,packages 沉淀复用能力,plugins 负责扩展,services 连接外部渠道。 这种分层背后有一个很现实的目标:

在保持产品迭代速度的同时,把"可复用能力"和"可扩展边界"稳定下来。否则功能一多,项目就会很快进入"每加一个能力都要改全局"的状态。

二、请求生命周期:先治理入口,再进入业务

AIRI 的服务入口不是"收到请求就直接执行业务",而是先做基础治理:跨域策略、会话中间件、请求体限制、观测链路,然后才分发到聊天、Provider、角色等路由。

这一步的价值在于把稳定性问题前置,而不是留给业务代码兜底。

path:apps/server/src/app.ts

php 复制代码
const app = new Hono<HonoEnv>()
  .use(
    '/api/*',
    cors({
      origin: origin => getTrustedOrigin(origin),
      credentials: true,
    }),
  )
  .use(honoLogger())
if (otel) {
  app.use('*', otelMiddleware(otel))
}
return app
  .use('*', sessionMiddleware(auth))
  .use('*', bodyLimit({ maxSize: 1024 * 1024 }))
  .route('/api/providers', createProviderRoutes(providerService))
  .route('/api/chats', createChatRoutes(chatService))

AIRI 把请求处理拆成了"治理层 + 业务层",属于典型的系统化服务结构。

三、Provider 管理:不是配置项,而是系统资源(精修版)

AIRI 把模型 Provider 当成"用户可管理的资源"来做,而不是把 API Key 直接散落在前端配置里。 从路由实现可以看到两层约束:先通过 authGuard 作为权限入口;创建时用 CreateProviderConfigSchema 做结构化校验,并把 ownerId 绑定到当前用户;修改时走 patch 路由,先加载目标配置,再用 existing.ownerId !== user.id 校验归属,不属于当前用户的改动会被直接拒绝。

path:apps/server/src/routes/providers.ts

dart 复制代码
export function createProviderRoutes(providerService: ProviderService) {
  return new Hono<HonoEnv>()
    .use('*', authGuard)
    .post('/', async (c) => {
      const user = c.get('user')!
      const body = await c.req.json()
      const result = safeParse(CreateProviderConfigSchema, body)

      if (!result.success) {
        throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues)
      }

      const provider = await providerService.createUserConfig({
        ...result.output,
        ownerId: user.id,
      })

      return c.json(provider, 201)
    })
    .patch('/:id', async (c) => {
      const user = c.get('user')!
      const id = c.req.param('id')
      const body = await c.req.json()
      const result = safeParse(UpdateProviderConfigSchema, body)

      if (!result.success) {
        throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', result.issues)
      }

      const existing = await providerService.findUserConfigById(id)
      if (!existing) throw createNotFoundError()
      if (existing.ownerId !== user.id) throw createForbiddenError()

      const updated = await providerService.updateUserConfig(id, result.output)
      return c.json(updated)
    })
}

这类实现的工程意义在于:Provider 的修改边界由后端路由用 ownerId 校验强制固定下来,而不是依赖前端或约定维持。

四、插件机制:能扩展,也要可控

AIRI 的插件扩展不是"把能力塞进系统就算完成",而是把插件当成需要长期协作的模块来管理。

插件宿主用状态机约束生命周期阶段:状态机覆盖这些阶段,再到需要配置、完成配置,最终进入就绪状态;失败会进入统一的失败阶段。

这样系统在任何时刻都能回答一个工程问题:插件现在处于什么阶段、下一步应该做什么、失败该怎么被观测与处理。 同时,宿主在插件调用能力前还会做权限断言:扩展能力可以被接入,但不会默认获得越权操作的能力边界。

path:packages/plugin-sdk/src/plugin-host/core.ts

php 复制代码
const pluginLifecycleMachine = createMachine({
  id: 'plugin-lifecycle',
  initial: 'loading',
  states: {
    loading: { on: { SESSION_LOADED: 'loaded', SESSION_FAILED: 'failed' } },
    loaded: { on: { START_AUTHENTICATION: 'authenticating', SESSION_FAILED: 'failed', STOP: 'stopped' } },
    authenticating: { on: { AUTHENTICATED: 'authenticated', SESSION_FAILED: 'failed' } },
    authenticated: { on: { ANNOUNCED: 'announced', SESSION_FAILED: 'failed' } },
    announced: { on: { START_PREPARING: 'preparing', CONFIGURATION_NEEDED: 'configuration-needed', STOP: 'stopped', SESSION_FAILED: 'failed' } },
    preparing: { on: { WAITING_DEPENDENCIES: 'waiting-deps', PREPARED: 'prepared', SESSION_FAILED: 'failed' } },
    prepared: { on: { CONFIGURATION_NEEDED: 'configuration-needed', CONFIGURED: 'configured', SESSION_FAILED: 'failed' } },
    configuration-needed: { on: { CONFIGURED: 'configured', SESSION_FAILED: 'failed' } },
    configured: { on: { READY: 'ready', SESSION_FAILED: 'failed' } },
    ready: { on: { REANNOUNCE: 'announced', CONFIGURATION_NEEDED: 'configuration-needed', STOP: 'stopped', SESSION_FAILED: 'failed' } },
    failed: { on: { STOP: 'stopped' } },
  },
})


private assertPermission(session: PluginHostSession, input: { area: 'apis'|'resources'|'capabilities'|'processors'|'pipelines'; action: string; key: string; reason?: string }) {
  const allowed = this.permissions.isAllowed(this.getPermissionScopeKey(session), input.area, input.action, input.key)
  if (allowed) return
  const error = new PermissionDeniedError({ area: input.area, action: input.action, key: input.key })
  session.channels.host.emit(errorPermission, { identity: session.identity, error: { area: input.area, action: input.action, key: input.key, recoverable: true } })
  throw error
}

扩展增长可以持续,但增长要留在契约和权限边界内。

五、执行闭环:不止"会回复",还要"会把事做完"

AIRI 在渠道服务(如 Telegram)里的实现,已经体现了典型 agent loop: 先根据上下文推断动作,再执行动作,必要时进入下一轮循环。

path:services/telegram-bot/src/bots/telegram/index.ts

ini 复制代码
const action = await imagineAnAction(
  ctx.bot.botInfo.id.toString(),
  currentController,
  chatCtx?.messages || [],
  chatCtx?.actions || [],
  { unreadMessages: ctx.unreadMessages, incomingMessages: [incomingMessage] },
)
return await dispatchAction(ctx, action, currentController, chatCtx)

while (typeof result === 'function') {
  result = await result()
}

六、如何理解这个仓库:一条更顺的阅读路径

如果要快速看懂 AIRI 的系统设计,可以按这个顺序:

apps/server:先看请求如何进入系统、入口如何治理 apps/:再看多端入口如何复用核心能力 packages/:看 Provider、Plugin、UI/runtime 的边界抽象 plugins 与 services:看扩展和外部渠道如何接入主链路 按这条路径阅读,会先建立系统主干,再进入扩展细节,不容易陷入局部代码。

结语

当一个 Agent 项目同时具备可治理的入口、可约束的扩展、可追踪的执行闭环,它才真正从"可演示"走向"可运行",这也是 AIRI 最值得参考的地方,它把 AI 能力从功能展示,推进到了系统工程。

参考链接

Project AIRI:github.com/moeru-ai/ai...

相关推荐
江湖行骗老中医2 小时前
Pinia 是 Vue 的专属状态管理库
前端·javascript·vue.js
用户69371750013842 小时前
Android 开发,别只钻技术一亩三分地,也该学点“广度”了
android·前端·后端
郑鱼咚2 小时前
别再神化Spec了,它可能只是AI Coding的临时补丁
前端
张元清2 小时前
React 鼠标追踪与交互效果实战
前端·javascript·面试
MinterFusion2 小时前
HTML DOM元素的定位问题
前端·css·html
落魄江湖行2 小时前
入门篇六 Nuxt4错误处理:给应用装个安全气囊
前端·typescript·nuxt4
薛定猫AI2 小时前
【技术干货】用 design.md 驯服 AI 生成前端:从 Awesome Design 到工程化落地实践
前端·人工智能
kyriewen2 小时前
你的JS代码总在半夜崩溃?TypeScript来“上保险”了
前端·javascript·typescript
iReachers3 小时前
HTML打包EXE配置管理教程:多项目打包设置一键保存、加载与切换
java·前端·javascript