在 AI Agent 快速发展的这两年,很多项目都已经能做到"能聊、能演示、能截图。
但真正决定一个项目能走多远的,往往不是首屏效果,而是工程治理能力:请求怎么被接入、会话怎么被隔离、扩展怎么被约束、出错之后怎么继续稳定运行。
Project AIRI 值得借鉴的地方就在这里。 它不是把模型能力包一层 UI,而是在尝试把输入、推理、执行、反馈组织成一套可持续迭代的运行系统。

- GitHub:github.com/moeru-ai/ai...
一、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...