docs: Mitsein Agent 插件化设计 — 两个抽象,完整生命周期
11 个 Lifecycle Hooks(系统/会话/消息/编排四层) + Context Middleware Chain(有序纯函数变换) = 覆盖 Agent 从启动到关闭的完整生命周期 含四个完整插件示例:Billing / 内容安全 / RAG / 监控
This commit is contained in:
parent
9e25c320d8
commit
381edb605f
306
docs/shared/mitsein-agent-plugin-design.md
Normal file
306
docs/shared/mitsein-agent-plugin-design.md
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
# Mitsein Agent 插件化设计:两个抽象,完整生命周期
|
||||||
|
|
||||||
|
!!! info "作者"
|
||||||
|
沙洲 & 星月 🌙 | 2026-04-08
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心设计
|
||||||
|
|
||||||
|
Mitsein 的 Agent 插件化只需要**两个抽象**:
|
||||||
|
|
||||||
|
1. **Lifecycle Hooks** — Agent 生命周期各阶段的拦截点
|
||||||
|
2. **Context Middleware Chain** — 对 LLM 调用参数的上下文驱动变换链
|
||||||
|
|
||||||
|
这两个抽象覆盖了 Agent 从启动到关闭、从收到消息到返回响应的完整生命周期。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 抽象 1:Lifecycle Hooks
|
||||||
|
|
||||||
|
11 个 hook 点,分四层:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AgentLifecycleHooks {
|
||||||
|
// ═══ 系统层 ═══
|
||||||
|
on_startup(): void;
|
||||||
|
on_shutdown(): void;
|
||||||
|
|
||||||
|
// ═══ 会话层 ═══
|
||||||
|
on_session_start(session: Session): void;
|
||||||
|
on_session_end(session: Session): void;
|
||||||
|
|
||||||
|
// ═══ 消息层 ═══
|
||||||
|
on_message_received(msg: InboundMessage): InboundMessage | void;
|
||||||
|
on_message_sending(msg: OutboundMessage): OutboundMessage | { cancel: true } | void;
|
||||||
|
|
||||||
|
// ═══ 心跳 ═══
|
||||||
|
on_heartbeat(ctx: HeartbeatContext): void;
|
||||||
|
|
||||||
|
// ═══ 编排层 ═══
|
||||||
|
before_llm_call(params: LlmCallParams): LlmCallParams | void;
|
||||||
|
after_llm_call(response: LlmResponse): LlmResponse | void;
|
||||||
|
before_tool_call(call: ToolCall): ToolCall | { block: true } | void;
|
||||||
|
after_tool_call(result: ToolResult): ToolResult | void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 层次模型
|
||||||
|
|
||||||
|
```
|
||||||
|
系统层 on_startup ──── on_heartbeat (定时) ──── on_shutdown
|
||||||
|
│ ▲
|
||||||
|
▼ │
|
||||||
|
会话层 on_session_start ────────────────── on_session_end
|
||||||
|
│ ▲
|
||||||
|
▼ │
|
||||||
|
消息层 on_message_received ────────── on_message_sending
|
||||||
|
│ ▲
|
||||||
|
▼ │
|
||||||
|
编排层 before_llm_call ──→ LLM ──→ after_llm_call
|
||||||
|
before_tool_call ─→ Tool ─→ after_tool_call
|
||||||
|
```
|
||||||
|
|
||||||
|
每层关注不同粒度。插件**按需注册**,不需要实现全部 hook。
|
||||||
|
|
||||||
|
### Hook 语义
|
||||||
|
|
||||||
|
| Hook | 返回值语义 |
|
||||||
|
|:-----|:----------|
|
||||||
|
| 返回修改后的对象 | 替换原始输入(拦截并修改) |
|
||||||
|
| 返回 `void` / `undefined` | 不修改,继续执行 |
|
||||||
|
| 返回 `{ block: true }` | 阻止执行(仅 `before_tool_call`) |
|
||||||
|
| 返回 `{ cancel: true }` | 取消发送(仅 `on_message_sending`) |
|
||||||
|
| 抛异常 | 中断整个链路 |
|
||||||
|
|
||||||
|
多个插件注册同一 hook 时按优先级顺序执行,前一个的输出是后一个的输入(pipeline 模式)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 抽象 2:Context Middleware Chain
|
||||||
|
|
||||||
|
一组有序的中间件,每个基于当前上下文对 LLM 调用参数产生副作用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ContextMiddleware = (
|
||||||
|
ctx: OrchestratorContext,
|
||||||
|
params: LlmCallParams,
|
||||||
|
) => LlmCallParams;
|
||||||
|
```
|
||||||
|
|
||||||
|
### OrchestratorContext
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface OrchestratorContext {
|
||||||
|
// 会话
|
||||||
|
session: Session;
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
// 对话
|
||||||
|
messages: Message[]; // 当前对话历史
|
||||||
|
lastToolResults: ToolResult[]; // 上一轮工具执行结果
|
||||||
|
|
||||||
|
// Agent 配置
|
||||||
|
agentConfig: AgentConfig; // YAML 定义的 agent 配置
|
||||||
|
availableTools: ToolDef[]; // 当前可用工具列表
|
||||||
|
|
||||||
|
// 运行时
|
||||||
|
turnIndex: number; // 当前对话轮次
|
||||||
|
parentContext?: OrchestratorContext; // 嵌套调用时的父上下文
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### LlmCallParams
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface LlmCallParams {
|
||||||
|
model: string;
|
||||||
|
messages: LlmMessage[];
|
||||||
|
tools?: ToolSchema[];
|
||||||
|
temperature?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
systemPrompt?: string;
|
||||||
|
// ...其他 LLM API 参数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 特征
|
||||||
|
|
||||||
|
- **有序执行** — 顺序可配置,有明确的优先级
|
||||||
|
- **纯函数式** — 输入 context + params,输出 params,无隐式副作用
|
||||||
|
- **可独立测试** — 每个 middleware 可以单独 mock context 测试
|
||||||
|
- **可组合** — 插件注册自己的 middleware,与核心 middleware 共存
|
||||||
|
|
||||||
|
### 注册
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 核心自带的 middleware(不可移除,可覆盖)
|
||||||
|
orchestrator.use(buildConversationHistory, { priority: 0 });
|
||||||
|
orchestrator.use(truncateToContextWindow, { priority: 100 });
|
||||||
|
|
||||||
|
// 插件注册的 middleware
|
||||||
|
orchestrator.use(selectModelByPlan, { priority: 10 });
|
||||||
|
orchestrator.use(injectRAGContext, { priority: 50 });
|
||||||
|
orchestrator.use(applyThinkingMode, { priority: 60 });
|
||||||
|
```
|
||||||
|
|
||||||
|
执行顺序按 priority 升序。同 priority 按注册顺序。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 插件注册
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AgentPlugin {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
hooks?: Partial<AgentLifecycleHooks>;
|
||||||
|
middlewares?: Array<{
|
||||||
|
fn: ContextMiddleware;
|
||||||
|
priority?: number; // 默认 50
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerPlugin(plugin: AgentPlugin): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### Billing 插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerPlugin({
|
||||||
|
id: "billing",
|
||||||
|
name: "Usage Billing",
|
||||||
|
version: "1.0.0",
|
||||||
|
hooks: {
|
||||||
|
on_session_start(session) {
|
||||||
|
initBillingSession(session.user);
|
||||||
|
},
|
||||||
|
before_llm_call(params) {
|
||||||
|
if (getUserCredits(params) <= 0) {
|
||||||
|
throw new InsufficientCreditsError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
after_llm_call(response) {
|
||||||
|
deductCredits(response.usage);
|
||||||
|
},
|
||||||
|
on_session_end(session) {
|
||||||
|
finalizeBilling(session);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
middlewares: [{
|
||||||
|
fn: (ctx, params) => ({
|
||||||
|
...params,
|
||||||
|
model: ctx.user.plan === "pro" ? "claude-sonnet-4" : "claude-haiku-4",
|
||||||
|
}),
|
||||||
|
priority: 10,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 内容安全插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerPlugin({
|
||||||
|
id: "content-safety",
|
||||||
|
name: "Content Safety Filter",
|
||||||
|
version: "1.0.0",
|
||||||
|
hooks: {
|
||||||
|
on_message_received(msg) {
|
||||||
|
if (containsBlockedContent(msg.text)) {
|
||||||
|
return { ...msg, blocked: true, reason: "policy_violation" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_message_sending(msg) {
|
||||||
|
return { ...msg, text: redactPII(msg.text) };
|
||||||
|
},
|
||||||
|
before_tool_call(call) {
|
||||||
|
if (isDangerousTool(call.name)) {
|
||||||
|
return { block: true };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### RAG 插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerPlugin({
|
||||||
|
id: "rag",
|
||||||
|
name: "Retrieval Augmented Generation",
|
||||||
|
version: "1.0.0",
|
||||||
|
middlewares: [{
|
||||||
|
fn: async (ctx, params) => {
|
||||||
|
const lastUserMsg = ctx.messages.findLast(m => m.role === "user");
|
||||||
|
const docs = await vectorSearch(lastUserMsg.content);
|
||||||
|
return {
|
||||||
|
...params,
|
||||||
|
systemPrompt: params.systemPrompt + formatRetrievedDocs(docs),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
priority: 50,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监控插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerPlugin({
|
||||||
|
id: "monitoring",
|
||||||
|
name: "Observability",
|
||||||
|
version: "1.0.0",
|
||||||
|
hooks: {
|
||||||
|
on_startup() {
|
||||||
|
initMetricsCollector();
|
||||||
|
},
|
||||||
|
on_heartbeat(ctx) {
|
||||||
|
reportHealthMetrics(ctx);
|
||||||
|
},
|
||||||
|
after_llm_call(response) {
|
||||||
|
recordLatency(response.model, response.timing);
|
||||||
|
recordTokenUsage(response.model, response.usage);
|
||||||
|
},
|
||||||
|
after_tool_call(result) {
|
||||||
|
recordToolExecution(result.toolName, result.duration, result.success);
|
||||||
|
},
|
||||||
|
on_shutdown() {
|
||||||
|
flushMetrics();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设计决策
|
||||||
|
|
||||||
|
### 为什么 Hook 而不是事件?
|
||||||
|
|
||||||
|
Hook 是**同步拦截**(可以修改输入输出),事件是**异步通知**(只能观察)。Agent 编排需要拦截能力——比如 `before_llm_call` 要能改 model,`before_tool_call` 要能 block。纯事件做不到。
|
||||||
|
|
||||||
|
### 为什么 Middleware 是独立抽象?
|
||||||
|
|
||||||
|
`before_llm_call` hook 虽然也能修改 params,但它是"一锤子"的——一个 hook 做一件事。Middleware chain 是"流水线"的——多个 middleware 依次变换 params,每个只关注一个维度(选模型、裁历史、注入 context...)。两者互补:
|
||||||
|
|
||||||
|
- Hook = 拦截/决策(要不要做、做了之后怎么办)
|
||||||
|
- Middleware = 变换/准备(怎么准备 LLM 的输入)
|
||||||
|
|
||||||
|
### 为什么不用 OpenClaw 的 44-hook 模式?
|
||||||
|
|
||||||
|
OpenClaw 把 Provider 注册(catalog/auth/诊断)和 Orchestrator 运行时(params/stream/replay)混在同一个对象里,导致 hook 数量膨胀。我们做了关注点分离:
|
||||||
|
|
||||||
|
- Provider 注册 = 静态元数据声明(不在这个插件体系里)
|
||||||
|
- Agent 运行时 = 两个抽象(11 hooks + middleware chain)
|
||||||
|
|
||||||
|
分离后,运行时层更简洁、更易理解、更好测试。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*沙洲 & 星月 🌙 — 2026-04-08*
|
||||||
Loading…
x
Reference in New Issue
Block a user