mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 17:51:20 +08:00
2239 lines
62 KiB
Markdown
2239 lines
62 KiB
Markdown
# Agent 消息处理流程规范
|
||
|
||
## 概述
|
||
|
||
AstrBot Agent 采用**双缓冲区 + 流控**的消息处理模型,实现消息的削峰填谷、限流保护和安全处理。
|
||
|
||
**核心设计**:
|
||
- **输入缓冲区**:用户消息暂存,按频率控制消费
|
||
- **输出缓冲区**:回复消息暂存,按策略分发
|
||
- **流控引擎**:根据 API 限制自动调节消费速率
|
||
- **安全层**:防注入、防泄密、防误触
|
||
|
||
## 架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Platform Adapter │
|
||
│ (QQ / Telegram / Discord / ...) │
|
||
└────────────────────────────┬────────────────────────────────────┘
|
||
│ commit_event()
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Input Message Buffer │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ UserQueue (per user/conversation) │ │
|
||
│ │ - metadata: user_id, platform, timestamp, session_id │ │
|
||
│ │ - messages: [msg1, msg2, ...] │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ FlowControl │
|
||
│ (rate limiter) │
|
||
└───────────────────────────┼─────────────────────────────────────┘
|
||
│ pull_messages()
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Agent Core │
|
||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||
│ │ Context │───▶│ LLM Loop │───▶│ Tool Call │ │
|
||
│ │ Manager │ │ (step loop) │ │ Executor │ │
|
||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||
└───────────────────────────┬─────────────────────────────────────┘
|
||
│ produce_result()
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Output Buffer │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ ResultQueue (per session) │ │
|
||
│ │ - content: string / stream │ │
|
||
│ │ - format: plain / markdown / html │ │
|
||
│ │ - strategy: streaming / segmented / full │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ DispatchStrategy │
|
||
│ (streaming / segmented / full) │
|
||
└───────────────────────────┼─────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Platform Adapter │
|
||
│ (SendResult) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 1. 工具、技能与 Agent 协作体系
|
||
|
||
### 1.1 三层架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Agent Core (LLM Loop) │
|
||
│ │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ Internal │ │ MCP │ │ Skills │ │
|
||
│ │ Tools │ │ Tools │ │ │ │
|
||
│ │ (Function │ │ (MCP │ │ (Pre-built │ │
|
||
│ │ Tool) │ │ Client) │ │ Agent │ │
|
||
│ │ │ │ │ │ Flows) │ │
|
||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||
│ │ │ │ │
|
||
│ └───────────────────┴───────────────────┘ │
|
||
│ │ │
|
||
│ Tool Executor │
|
||
└──────────────────────────────┼──────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Agent 协作层 │
|
||
│ │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ 本地 │ │ 远程 │ │ 子 Agent │ │
|
||
│ │ Subagent │ │ A2A Agent │ │ (MCP/A2A) │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ ACP 协议 (Agent 通信) │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.2 工具来源
|
||
|
||
| 来源 | 协议 | 说明 |
|
||
|------|------|------|
|
||
| **Internal Tools** | 自定义 Python | `FunctionTool`/`ToolSet`,Star 插件注册 |
|
||
| **MCP Tools** | MCP JSON-RPC 2.0 | 外部 MCP 服务器提供的工具 |
|
||
| **Skills** | 自定义协议 | 预构建的 Agent 执行流程模板 |
|
||
|
||
### 1.3 工具调用决策
|
||
|
||
```python
|
||
class ToolRouter:
|
||
"""工具路由"""
|
||
|
||
def __init__(
|
||
self,
|
||
internal_toolset: ToolSet,
|
||
mcp_clients: dict[str, MCPClient],
|
||
skill_executors: dict[str, SkillExecutor],
|
||
):
|
||
self.internal = internal_toolset
|
||
self.mcp = mcp_clients
|
||
self.skills = skill_executors
|
||
|
||
async def route_tool_call(
|
||
self,
|
||
tool_name: str,
|
||
arguments: dict,
|
||
context: AgentContext,
|
||
) -> ToolResult:
|
||
"""路由工具调用"""
|
||
|
||
# 1. 检查内部工具
|
||
internal_tool = self.internal.get_tool(tool_name)
|
||
if internal_tool:
|
||
return await self._call_internal(internal_tool, arguments, context)
|
||
|
||
# 2. 检查 MCP 工具
|
||
for client_name, client in self.mcp.items():
|
||
if client.has_tool(tool_name):
|
||
return await client.call_tool(tool_name, arguments)
|
||
|
||
# 3. 检查 Skills
|
||
skill = self.skills.get(tool_name)
|
||
if skill:
|
||
return await self._execute_skill(skill, arguments, context)
|
||
|
||
raise ToolNotFoundError(f"Tool not found: {tool_name}")
|
||
```
|
||
|
||
### 1.4 Agent 协作(ACP 协议)
|
||
|
||
```python
|
||
class ACPAgentClient:
|
||
"""ACP Agent 客户端"""
|
||
|
||
async def call_agent(
|
||
self,
|
||
agent_name: str,
|
||
action: str,
|
||
args: dict,
|
||
stream: bool = True,
|
||
) -> AsyncIterator[AgentEvent] | AgentResult:
|
||
"""调用远程 Agent"""
|
||
|
||
request = ACPRequest(
|
||
method="agent/call",
|
||
params={
|
||
"agent": agent_name,
|
||
"action": action,
|
||
"args": args,
|
||
}
|
||
)
|
||
|
||
if stream:
|
||
return self._stream_request(request)
|
||
else:
|
||
return await self._send_request(request)
|
||
|
||
async def list_agents(self) -> list[AgentCard]:
|
||
"""列出可用 Agent"""
|
||
response = await self._send_request(
|
||
ACPRequest(method="agent/list")
|
||
)
|
||
return [AgentCard(**a) for a in response.result["agents"]]
|
||
```
|
||
|
||
### 1.5 Skills 执行
|
||
|
||
```python
|
||
class SkillExecutor:
|
||
"""Skill 执行器"""
|
||
|
||
def __init__(self, skill_registry: SkillRegistry):
|
||
self.registry = skill_registry
|
||
|
||
async def execute(
|
||
self,
|
||
skill_name: str,
|
||
input_data: dict,
|
||
context: AgentContext,
|
||
) -> SkillResult:
|
||
"""执行 Skill"""
|
||
|
||
skill = self.registry.get(skill_name)
|
||
if not skill:
|
||
raise SkillNotFoundError(f"Skill not found: {skill_name}")
|
||
|
||
# Skill 可以包含多个步骤
|
||
steps = skill.get_steps()
|
||
|
||
results = []
|
||
for step in steps:
|
||
# 每个步骤可以是工具调用或 Agent 调用
|
||
if step.type == "tool":
|
||
result = await self._call_tool(step.tool, step.args)
|
||
elif step.type == "agent":
|
||
result = await self._call_agent(step.agent, step.action, step.args)
|
||
elif step.type == "llm":
|
||
result = await self._call_llm(step.prompt, context)
|
||
|
||
results.append(result)
|
||
|
||
# 检查是否需要停止
|
||
if step.on_result == "stop_if_success" and result.success:
|
||
break
|
||
|
||
return SkillResult(
|
||
skill_name=skill_name,
|
||
steps=results,
|
||
final_output=results[-1] if results else None,
|
||
)
|
||
```
|
||
|
||
### 1.6 配置
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
|
||
# 工具配置
|
||
tools:
|
||
# 内部工具
|
||
internal:
|
||
enabled: true
|
||
max_per_request: 128
|
||
|
||
# MCP 工具
|
||
mcp:
|
||
enabled: true
|
||
servers: [] # MCP 服务器配置
|
||
|
||
# Skills
|
||
skills:
|
||
enabled: true
|
||
registry_path: "$XDG_DATA_HOME/astrbot/skills/"
|
||
|
||
# Agent 协作配置
|
||
agent_collaboration:
|
||
# ACP 配置
|
||
acp:
|
||
enabled: true
|
||
endpoints:
|
||
- name: "local"
|
||
type: "unix"
|
||
path: "/run/astrbot/acp.sock"
|
||
|
||
# 子 Agent 配置
|
||
subagents:
|
||
enabled: true
|
||
max_parallel: 3
|
||
timeout: 300
|
||
|
||
# Agent 发现
|
||
discovery:
|
||
# 自动发现同进程内的 Subagent
|
||
auto_discover_internal: true
|
||
|
||
# 定期刷新远程 Agent 列表
|
||
refresh_interval: 60
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 输入缓冲区(Input Buffer)
|
||
|
||
### 2.1 队列结构
|
||
|
||
```rust
|
||
use serde::{Deserialize, Serialize};
|
||
use std::collections::VecDeque;
|
||
use std::sync::Arc;
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct InputMessage {
|
||
/// 全局唯一 ID
|
||
pub message_id: String,
|
||
/// 平台标识
|
||
pub platform: String,
|
||
/// 用户 ID
|
||
pub user_id: String,
|
||
/// 会话 ID
|
||
pub conversation_id: String,
|
||
/// 消息内容
|
||
pub content: MessageContent,
|
||
/// 到达时间
|
||
pub timestamp: f64,
|
||
/// 扩展元数据
|
||
pub metadata: HashMap<String, String>,
|
||
/// 优先级(越高越先处理)
|
||
#[serde(default)]
|
||
pub priority: i32,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub enum MessageContent {
|
||
Plain(String),
|
||
Chain(Vec<MessageSegment>),
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct MessageSegment {
|
||
pub segment_type: String,
|
||
pub content: String,
|
||
#[serde(default)]
|
||
pub metadata: HashMap<String, String>,
|
||
}
|
||
|
||
pub struct UserMessageQueue {
|
||
pub user_id: String,
|
||
pub session_id: String,
|
||
messages: VecDeque<InputMessage>,
|
||
metadata: HashMap<String, String>,
|
||
pub created_at: f64,
|
||
pub updated_at: f64,
|
||
pub max_size: usize,
|
||
pub max_age: f64,
|
||
}
|
||
|
||
impl UserMessageQueue {
|
||
pub fn new(user_id: String, session_id: String) -> Self {
|
||
let now = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_secs_f64();
|
||
|
||
Self {
|
||
user_id,
|
||
session_id,
|
||
messages: VecDeque::new(),
|
||
metadata: HashMap::new(),
|
||
created_at: now,
|
||
updated_at: now,
|
||
max_size: 1000,
|
||
max_age: 3600.0,
|
||
}
|
||
}
|
||
|
||
pub fn push(&mut self, msg: InputMessage) {
|
||
self.messages.push_back(msg);
|
||
self.updated_at = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_secs_f64();
|
||
}
|
||
|
||
pub fn pop(&mut self) -> Option<InputMessage> {
|
||
self.messages.pop_front()
|
||
}
|
||
|
||
pub fn len(&self) -> usize {
|
||
self.messages.len()
|
||
}
|
||
|
||
pub fn is_empty(&self) -> bool {
|
||
self.messages.is_empty()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.2 缓冲区配置
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
input_buffer:
|
||
# 单用户队列最大消息数
|
||
max_queue_size: 1000
|
||
|
||
# 消息最大存活时间(秒)
|
||
max_message_age: 3600
|
||
|
||
# 超出限制时的处理策略
|
||
overflow_strategy: "drop_oldest" # drop_oldest | drop_newest | block
|
||
|
||
# 丢弃消息时的提示前缀
|
||
overflow_hint: "[消息过多,部分早期消息已丢弃]"
|
||
|
||
# 是否按用户隔离队列
|
||
per_user_queue: true
|
||
|
||
# 是否按会话隔离队列
|
||
per_conversation_queue: true
|
||
```
|
||
|
||
### 2.3 溢出保护策略
|
||
|
||
| 策略 | 说明 | 适用场景 |
|
||
|------|------|----------|
|
||
| `drop_oldest` | 丢弃最旧的消息,保留最新的 | 高频聊天,侧重时效性 |
|
||
| `drop_newest` | 丢弃最新的消息,保留旧的 | 重要指令,不容丢失 |
|
||
| `block` | 阻塞输入,直到队列有空位 | 重要对话,不容任何丢弃 |
|
||
|
||
**溢出时的处理**:
|
||
|
||
```rust
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum OverflowStrategy {
|
||
DropOldest,
|
||
DropNewest,
|
||
Block,
|
||
}
|
||
|
||
pub struct InputBuffer {
|
||
queues: HashMap<String, Arc<tokio::sync::Mutex<UserMessageQueue>>>,
|
||
overflow_strategy: OverflowStrategy,
|
||
overflow_hint: String,
|
||
}
|
||
|
||
impl InputBuffer {
|
||
/// 添加消息到队列
|
||
pub async fn add_message(&self, queue_id: &str, message: InputMessage) -> Result<(), BufferError> {
|
||
let queue = self.queues.get(queue_id)
|
||
.ok_or(BufferError::QueueNotFound)?;
|
||
|
||
let mut queue = queue.lock().await;
|
||
|
||
if queue.messages.len() >= queue.max_size {
|
||
match self.overflow_strategy {
|
||
OverflowStrategy::DropOldest => {
|
||
if let Some(old_msg) = queue.messages.pop_front() {
|
||
// 在丢弃的消息前插入提示
|
||
let hint = InputMessage {
|
||
message_id: "system_hint".into(),
|
||
content: MessageContent::Plain(format!(
|
||
"[{} 丢弃于 {}]",
|
||
self.overflow_hint,
|
||
old_msg.timestamp
|
||
)),
|
||
..message.clone()
|
||
};
|
||
queue.messages.push_front(hint);
|
||
}
|
||
queue.messages.push_back(message);
|
||
}
|
||
OverflowStrategy::DropNewest => {
|
||
// 丢弃新消息,不插入
|
||
}
|
||
OverflowStrategy::Block => {
|
||
// 等待直到队列有空位
|
||
while queue.messages.len() >= queue.max_size {
|
||
let queue_clone = queue.clone();
|
||
drop(queue);
|
||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||
queue = queue_clone.lock().await;
|
||
}
|
||
queue.messages.push_back(message);
|
||
}
|
||
}
|
||
} else {
|
||
queue.messages.push_back(message);
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 流控引擎(Flow Control)
|
||
|
||
### 3.1 速率限制配置
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
flow_control:
|
||
# 消费速率模式
|
||
mode: "auto" # auto | manual
|
||
|
||
# 手动模式:每秒处理消息数
|
||
manual_rate: 10
|
||
|
||
# 自动模式:基于 LLM API 限制计算
|
||
auto:
|
||
# LLM API 每分钟请求限制
|
||
api_rpm_limit: 60
|
||
|
||
# 每次请求预计处理消息数
|
||
messages_per_request: 5
|
||
|
||
# 安全系数(留一定余量)
|
||
safety_margin: 0.8
|
||
|
||
# 最小消费间隔(秒)
|
||
min_interval: 0.5
|
||
|
||
# 最大消费间隔(秒)
|
||
max_interval: 10
|
||
```
|
||
|
||
### 3.2 速率计算公式
|
||
|
||
```
|
||
effective_rate = min(api_rpm_limit * messages_per_request * safety_margin, 1/min_interval)
|
||
consume_interval = 1 / effective_rate
|
||
```
|
||
|
||
**示例**:
|
||
- API RPM = 60
|
||
- 每请求处理 5 条消息
|
||
- 安全系数 = 0.8
|
||
- 有效速率 = 60 * 5 * 0.8 = 240 消息/分钟 = 4 消息/秒
|
||
- 消费间隔 = 0.25 秒
|
||
|
||
### 3.3 令牌桶实现
|
||
|
||
```python
|
||
class TokenBucket:
|
||
"""令牌桶流控"""
|
||
|
||
def __init__(
|
||
self,
|
||
rate: float, # 每秒令牌数
|
||
capacity: float, # 桶容量
|
||
burst: float = None, # 突发容量
|
||
):
|
||
self.rate = rate
|
||
self.capacity = capacity
|
||
self.burst = burst or capacity
|
||
self.tokens = capacity
|
||
self.last_update = time.monotonic()
|
||
|
||
async def acquire(self, tokens: float = 1.0) -> float:
|
||
"""获取令牌,返回需要等待的秒数"""
|
||
now = time.monotonic()
|
||
elapsed = now - self.last_update
|
||
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
|
||
self.last_update = now
|
||
|
||
if self.tokens >= tokens:
|
||
self.tokens -= tokens
|
||
return 0.0
|
||
|
||
wait_time = (tokens - self.tokens) / self.rate
|
||
return wait_time
|
||
|
||
async def wait_and_acquire(self, tokens: float = 1.0) -> None:
|
||
"""等待直到获取令牌"""
|
||
wait = await self.acquire(tokens)
|
||
if wait > 0:
|
||
await asyncio.sleep(wait)
|
||
```
|
||
|
||
### 3.4 优先级调度
|
||
|
||
```python
|
||
class PriorityScheduler:
|
||
"""优先级调度器"""
|
||
|
||
def __init__(self, buckets: dict[str, TokenBucket]):
|
||
self.buckets = buckets # per-user or per-session
|
||
|
||
async def next_message(self) -> InputMessage | None:
|
||
"""获取下一条待处理消息(按优先级)"""
|
||
# 1. 收集所有非空队列
|
||
candidates = []
|
||
for user_id, queue in self.queues.items():
|
||
if not queue.messages:
|
||
continue
|
||
|
||
# 2. 计算该用户的可用速率
|
||
bucket = self.buckets.get(user_id)
|
||
if not bucket:
|
||
continue
|
||
|
||
# 3. 获取队首消息(peek,不移除)
|
||
msg = queue.messages[0]
|
||
|
||
candidates.append((msg, bucket, user_id))
|
||
|
||
if not candidates:
|
||
return None
|
||
|
||
# 4. 按优先级 + 可用性排序
|
||
# 优先级相同时,优先处理令牌充足的
|
||
candidates.sort(
|
||
key=lambda x: (
|
||
-x[0].priority,
|
||
x[1].tokens / x[1].rate if x[1].rate > 0 else 0
|
||
)
|
||
)
|
||
|
||
# 5. 等待最紧急消息的令牌
|
||
msg, bucket, user_id = candidates[0]
|
||
await bucket.wait_and_acquire(1.0)
|
||
|
||
# 6. 移除并返回
|
||
return queue.messages.popleft()
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Agent 核心(Agent Core)
|
||
|
||
### 4.1 上下文管理(Context Manager)
|
||
|
||
```python
|
||
@dataclass
|
||
class AgentContext:
|
||
"""Agent 执行上下文"""
|
||
messages: list[Message] # 消息历史
|
||
system_prompt: str # 系统提示
|
||
tools: list[ToolDefinition] # 可用工具
|
||
memory: MemoryBank # 记忆存储
|
||
metadata: dict # 扩展元数据
|
||
|
||
class ContextManager:
|
||
"""上下文管理器"""
|
||
|
||
def __init__(self, config: ContextConfig):
|
||
self.max_tokens: int = config.max_context_tokens
|
||
self.compress_threshold: float = config.compress_threshold
|
||
self.keep_recent: int = config.keep_recent_messages
|
||
|
||
def build_context(
|
||
self,
|
||
queue: UserMessageQueue,
|
||
memory: MemoryBank,
|
||
) -> AgentContext:
|
||
"""构建 Agent 执行上下文"""
|
||
|
||
# 1. 从队列获取消息
|
||
raw_messages = list(queue.messages)
|
||
|
||
# 2. 应用安全过滤
|
||
raw_messages = self.apply_security_filters(raw_messages)
|
||
|
||
# 3. 构建消息列表
|
||
messages = self.build_message_list(raw_messages)
|
||
|
||
# 4. 检查是否需要压缩
|
||
total_tokens = self.estimate_tokens(messages)
|
||
|
||
if total_tokens > self.max_tokens * self.compress_threshold:
|
||
messages = self.compress_context(messages, memory)
|
||
|
||
# 5. 添加系统提示
|
||
system_prompt = self.build_system_prompt()
|
||
|
||
return AgentContext(
|
||
messages=messages,
|
||
system_prompt=system_prompt,
|
||
tools=self.get_available_tools(),
|
||
memory=memory,
|
||
)
|
||
|
||
def compress_context(
|
||
self,
|
||
messages: list[Message],
|
||
memory: MemoryBank,
|
||
) -> list[Message]:
|
||
"""压缩上下文"""
|
||
|
||
# 保留最近 N 条消息
|
||
recent = messages[-self.keep_recent:]
|
||
|
||
# 提取历史消息进行压缩
|
||
history = messages[:-self.keep_recent]
|
||
|
||
# 摘要历史消息并存入记忆
|
||
if history:
|
||
summary = self.summarize(history)
|
||
memory.add(Message(
|
||
role="system",
|
||
content=f"[历史摘要] {summary}",
|
||
metadata={"type": "summary"}
|
||
))
|
||
|
||
return recent
|
||
```
|
||
|
||
### 4.2 上下文配置
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
context:
|
||
# 最大上下文 token 数
|
||
max_context_tokens: 128000
|
||
|
||
# 触发压缩的阈值(比例)
|
||
compress_threshold: 0.85
|
||
|
||
# 压缩后保留的最近消息数
|
||
keep_recent_messages: 6
|
||
|
||
# 压缩提供者(为空则使用主 Provider)
|
||
compress_provider_id: ""
|
||
|
||
# 压缩提示词
|
||
compress_instruction: |
|
||
请简洁地总结对话要点,保留关键信息如:
|
||
- 用户的主要需求或问题
|
||
- 已确定的方案或结论
|
||
- 未完成的任务
|
||
|
||
# 消息保留策略
|
||
retention:
|
||
# 保留最近 N 小时内的原始消息
|
||
recent_hours: 24
|
||
|
||
# 超出后转为摘要存储
|
||
summarize_after: true
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 工具调用策略(Tool Calling Strategy)
|
||
|
||
### 4.1 工具调用最佳实践
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
tool_calling:
|
||
# 工具调用策略
|
||
strategy: "smart" # eager | sequential | smart
|
||
|
||
# 每次请求最大工具调用数
|
||
max_calls_per_request: 128
|
||
|
||
# 工具调用超时(秒)
|
||
timeout: 60
|
||
|
||
# 工具调用失败重试次数
|
||
max_retries: 3
|
||
|
||
# 是否并行调用独立工具
|
||
parallel_calls: true
|
||
|
||
# 并行调用最大数量
|
||
max_parallel_calls: 5
|
||
|
||
# 工具结果的最大 token 数(截断)
|
||
max_result_tokens: 4096
|
||
|
||
# 是否在工具调用后立即返回中间结果
|
||
stream_intermediate: true
|
||
```
|
||
|
||
### 4.2 工具调用流程
|
||
|
||
```python
|
||
class ToolCallingPolicy:
|
||
"""工具调用策略"""
|
||
|
||
async def execute_tools(
|
||
self,
|
||
llm_response: LLMResponse,
|
||
context: AgentContext,
|
||
) -> list[ToolResult]:
|
||
"""执行工具调用"""
|
||
|
||
# 1. 解析工具调用请求
|
||
tool_calls = llm_response.tool_calls or []
|
||
|
||
if not tool_calls:
|
||
return []
|
||
|
||
# 2. 按策略分组
|
||
groups = self._group_by_dependency(tool_calls)
|
||
|
||
results = []
|
||
|
||
# 3. 按组执行
|
||
for group in groups:
|
||
if self._can_parallel(group):
|
||
# 并行执行
|
||
group_results = await asyncio.gather(
|
||
*[self._execute_single(call, context) for call in group],
|
||
return_exceptions=True
|
||
)
|
||
else:
|
||
# 串行执行
|
||
group_results = []
|
||
for call in group:
|
||
result = await self._execute_single(call, context)
|
||
group_results.append(result)
|
||
|
||
results.extend(group_results)
|
||
|
||
# 4. 检查是否超过限制
|
||
if len(results) >= self.config.max_calls_per_request:
|
||
break
|
||
|
||
# 5. 如果需要流式中间结果
|
||
if self.config.stream_intermediate:
|
||
yield ToolCallingEvent(
|
||
type="intermediate",
|
||
results=group_results
|
||
)
|
||
|
||
return results
|
||
|
||
def _group_by_dependency(
|
||
self,
|
||
tool_calls: list[ToolCall],
|
||
) -> list[list[ToolCall]]:
|
||
"""按依赖关系分组"""
|
||
|
||
groups = []
|
||
current_group = []
|
||
|
||
for call in tool_calls:
|
||
# 检查是否依赖前一个工具的结果
|
||
if call.arguments_depends_on_previous and current_group:
|
||
# 依赖:将当前调用加入前一个组
|
||
current_group.append(call)
|
||
else:
|
||
# 不依赖:开启新组
|
||
if current_group:
|
||
groups.append(current_group)
|
||
current_group = [call]
|
||
|
||
if current_group:
|
||
groups.append(current_group)
|
||
|
||
return groups
|
||
```
|
||
|
||
### 4.3 工具选择策略
|
||
|
||
```python
|
||
class ToolSelector:
|
||
"""工具选择策略"""
|
||
|
||
def __init__(self, config: ToolSelectionConfig):
|
||
self.max_tools_per_request = config.max_tools_per_request
|
||
self.prefer_recent = config.prefer_recent_tools
|
||
|
||
def select_tools(
|
||
self,
|
||
available_tools: list[Tool],
|
||
query: str,
|
||
context: AgentContext,
|
||
) -> list[Tool]:
|
||
"""选择最相关的工具"""
|
||
|
||
# 1. 计算工具与查询的相关性
|
||
scored = []
|
||
for tool in available_tools:
|
||
score = self._calculate_relevance(tool, query, context)
|
||
scored.append((score, tool))
|
||
|
||
# 2. 排序并截取
|
||
scored.sort(key=lambda x: -x[0])
|
||
selected = scored[:self.max_tools_per_request]
|
||
|
||
# 3. 如果启用了最近使用优先
|
||
if self.prefer_recent:
|
||
selected = self._boost_recent(selected, context)
|
||
|
||
return [t for _, t in selected]
|
||
|
||
def _calculate_relevance(
|
||
self,
|
||
tool: Tool,
|
||
query: str,
|
||
context: AgentContext,
|
||
) -> float:
|
||
"""计算相关性分数"""
|
||
|
||
base_score = 0.0
|
||
|
||
# 工具名称匹配
|
||
if any(word in tool.name.lower() for word in query.lower().split()):
|
||
base_score += 0.3
|
||
|
||
# 工具描述匹配
|
||
if tool.description:
|
||
# 简单的词重叠计算
|
||
query_words = set(query.lower().split())
|
||
desc_words = set(tool.description.lower().split())
|
||
overlap = len(query_words & desc_words)
|
||
base_score += overlap * 0.1
|
||
|
||
# 最近使用过的工具加权
|
||
if context.metadata.get("recent_tools"):
|
||
if tool.name in context.metadata["recent_tools"]:
|
||
base_score += 0.2
|
||
|
||
return base_score
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 安全层(Security Layer)
|
||
|
||
### 6.1 安全配置
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
security:
|
||
# 防注入配置
|
||
injection:
|
||
# 启用防注入
|
||
enable: true
|
||
|
||
# 检测模式
|
||
mode: "strict" # strict | moderate | permissive
|
||
|
||
# 注入模式识别
|
||
patterns:
|
||
- name: "role_play_injection"
|
||
regex: "(?i)(you are now|forget previous|ignore all)"
|
||
severity: "high"
|
||
|
||
- name: "system_prompt_leak"
|
||
regex: "(?i)(repeat your? (system|initial) (prompt|instructions))"
|
||
severity: "high"
|
||
|
||
- name: "code_injection"
|
||
regex: "(?i)(```(system|prompt|instructor))"
|
||
severity: "medium"
|
||
|
||
# 触发时的处理策略
|
||
on_detect: "sanitize" # sanitize | block | warn
|
||
|
||
# 是否记录检测日志
|
||
log_detections: true
|
||
|
||
# 内容过滤配置
|
||
content_filter:
|
||
# 启用内容过滤
|
||
enable: true
|
||
|
||
# 过滤级别
|
||
level: "standard" # strict | standard | minimal
|
||
|
||
# 敏感词列表(文件路径或内联)
|
||
blocklist: []
|
||
|
||
# 替换字符
|
||
replacement: "[已过滤]"
|
||
|
||
# 泄密防护
|
||
leakage_prevention:
|
||
# 阻止 Agent 读取敏感文件模式
|
||
blocked_file_patterns:
|
||
- "**/.env"
|
||
- "**/secrets.yaml"
|
||
- "**/*password*"
|
||
- "**/.git/credentials"
|
||
|
||
# 阻止 Agent 输出敏感信息模式
|
||
blocked_output_patterns:
|
||
- "(?i)api[_-]?key"
|
||
- "(?i)secret"
|
||
- "(?i)password"
|
||
|
||
# 替换为占位符
|
||
placeholder: "[REDACTED]"
|
||
```
|
||
|
||
### 6.2 安全过滤器实现
|
||
|
||
```python
|
||
class SecurityFilter:
|
||
"""安全过滤器"""
|
||
|
||
def __init__(self, config: SecurityConfig):
|
||
self.config = config
|
||
self.compiled_patterns = [
|
||
(p["name"], re.compile(p["regex"]), p["severity"])
|
||
for p in config.injection.patterns
|
||
]
|
||
|
||
def filter_messages(
|
||
self,
|
||
messages: list[InputMessage],
|
||
) -> list[InputMessage]:
|
||
"""过滤输入消息"""
|
||
|
||
filtered = []
|
||
|
||
for msg in messages:
|
||
# 1. 内容过滤
|
||
if self.config.content_filter.enable:
|
||
msg.content = self._filter_content(msg.content)
|
||
|
||
# 2. 注入检测
|
||
if self.config.injection.enable:
|
||
detections = self._detect_injection(msg.content)
|
||
|
||
if detections:
|
||
action = self._handle_injection(detections, msg)
|
||
if action == "skip":
|
||
continue
|
||
|
||
filtered.append(msg)
|
||
|
||
return filtered
|
||
|
||
def filter_output(
|
||
self,
|
||
content: str,
|
||
context: AgentContext,
|
||
) -> str:
|
||
"""过滤输出内容"""
|
||
|
||
# 1. 泄密防护 - 移除敏感信息
|
||
if self.config.leakage_prevention:
|
||
content = self._redact_sensitive(content)
|
||
|
||
return content
|
||
|
||
def _detect_injection(self, content: str) -> list[Detection]:
|
||
"""检测注入攻击"""
|
||
detections = []
|
||
|
||
for name, pattern, severity in self.compiled_patterns:
|
||
if pattern.search(content):
|
||
detections.append(Detection(
|
||
name=name,
|
||
severity=severity,
|
||
matched=pattern.findall(content),
|
||
))
|
||
|
||
return detections
|
||
|
||
def _handle_injection(
|
||
self,
|
||
detections: list[Detection],
|
||
message: InputMessage,
|
||
) -> str:
|
||
"""处理注入检测"""
|
||
|
||
high_severity = any(d.severity == "high" for d in detections)
|
||
|
||
if high_severity and self.config.injection.on_detect == "block":
|
||
# 记录并阻止
|
||
logging.warning(f"Blocked injection: {detections}")
|
||
return "skip"
|
||
|
||
elif self.config.injection.on_detect == "sanitize":
|
||
# 消毒处理
|
||
for detection in detections:
|
||
message.content = message.content.replace(
|
||
detection.matched,
|
||
self.config.content_filter.replacement,
|
||
)
|
||
return "sanitize"
|
||
|
||
return "allow"
|
||
|
||
def _filter_content(self, content: str) -> str:
|
||
"""内容过滤"""
|
||
|
||
if not self.config.content_filter.enable:
|
||
return content
|
||
|
||
for pattern in self.config.content_filter.blocklist:
|
||
content = re.sub(pattern, self.config.content_filter.replacement, content)
|
||
|
||
return content
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 权限模型(Permission Model)
|
||
|
||
### 7.1 设计原则
|
||
|
||
遵循 **Unix 哲学**,权限模型采用类似 `rwx` 的能力(Capability)设计:
|
||
|
||
| 原则 | 说明 |
|
||
|------|------|
|
||
| **最小权限** | 只授予完成任务所需的最小权限集 |
|
||
| **能力继承** | 高权限自动包含低权限的能力 |
|
||
| **可组合** | 权限可以灵活组合,适应不同场景 |
|
||
| **可委托** | 支持权限的委托和回收 |
|
||
|
||
### 7.2 角色定义
|
||
|
||
```rust
|
||
/// 角色枚举,类比 Unix 用户组
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
#[repr(u8)]
|
||
pub enum Role {
|
||
Owner = 0o700, // 超级管理员/拥有者
|
||
Admin = 0o600, // 普通管理员
|
||
Member = 0o400, // 普通成员
|
||
Guest = 0o100, // 访客(受限)
|
||
Blocked = 0o000, // 被封禁
|
||
}
|
||
|
||
bitflags::bitflags! {
|
||
/// 权限枚举,类比 rwx
|
||
pub struct Permission: u16 {
|
||
// 基础权限
|
||
const READ = 0o400; // 读取权限
|
||
const WRITE = 0o200; // 写入权限
|
||
const EXECUTE = 0o100; // 执行权限
|
||
|
||
// 消息权限
|
||
const SEND_MESSAGE = 0o040; // 发送消息
|
||
const SEND_MEDIA = 0o020; // 发送媒体
|
||
const SEND_COMMAND = 0o010; // 发送命令
|
||
|
||
// 管理权限
|
||
const MANAGE_MEMBER = 0o004; // 管理成员
|
||
const MANAGE_CONFIG = 0o002; // 管理配置
|
||
const MANAGE_PERMISSION = 0o001; // 管理权限
|
||
|
||
// 特殊权限
|
||
const BOT_ADMIN = 0o700; // Bot 管理员(全权限)
|
||
const OWNER_ONLY = 0o100; // 仅拥有者可用
|
||
}
|
||
}
|
||
|
||
impl Role {
|
||
/// 检查角色是否拥有指定权限
|
||
pub fn has_permission(&self, permission: Permission) -> bool {
|
||
let role_bits = self.bits();
|
||
(role_bits & permission.bits()) == permission.bits()
|
||
}
|
||
|
||
/// 获取角色的权限位
|
||
fn bits(&self) -> u16 {
|
||
*self as u16
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.3 能力矩阵
|
||
|
||
```
|
||
┌──────────────────┬───────┬───────┬────────┬────────┬──────────┐
|
||
│ 能力 │ OWNER │ ADMIN │ MEMBER │ GUEST │ BLOCKED │
|
||
├──────────────────┼───────┼───────┼────────┼────────┼──────────┤
|
||
│ 读取消息 │ ✓ │ ✓ │ ✓ │ ✓ │ ✗ │
|
||
│ 发送普通消息 │ ✓ │ ✓ │ ✓ │ ✓ │ ✗ │
|
||
│ 发送媒体 │ ✓ │ ✓ │ ✓ │ ✗ │ ✗ │
|
||
│ 发送斜杠命令 │ ✓ │ ✓ │ ✓ │ ✗ │ ✗ │
|
||
│ 使用管理员命令 │ ✓ │ ✓ │ ✗ │ ✗ │ ✗ │
|
||
│ 管理成员 │ ✓ │ ✓ │ ✗ │ ✗ │ ✗ │
|
||
│ 修改配置 │ ✓ │ ✗ │ ✗ │ ✗ │ ✗ │
|
||
│ 转让所有权 │ ✓ │ ✗ │ ✗ │ ✗ │ ✗ │
|
||
│ 踢出 Bot │ ✓ │ ✗ │ ✗ │ ✗ │ ✗ │
|
||
└──────────────────┴───────┴───────┴────────┴────────┴──────────┘
|
||
```
|
||
|
||
### 7.4 权限检查流程
|
||
|
||
```rust
|
||
use async_trait::async_trait;
|
||
|
||
#[async_trait]
|
||
pub trait PermissionCheck {
|
||
async fn check_message(
|
||
&self,
|
||
event: &InputMessage,
|
||
context: &AgentContext,
|
||
) -> PermissionResult;
|
||
}
|
||
|
||
pub struct PermissionMiddleware {
|
||
role_config: RoleConfig,
|
||
command_permissions: HashMap<String, Permission>,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct PermissionResult {
|
||
pub allowed: bool,
|
||
pub reason: Option<String>,
|
||
}
|
||
|
||
impl PermissionMiddleware {
|
||
/// 检查消息权限
|
||
async fn check_message(
|
||
&self,
|
||
event: &InputMessage,
|
||
context: &AgentContext,
|
||
) -> PermissionResult {
|
||
// 1. 获取发送者角色
|
||
let role = self
|
||
.get_user_role(&event.user_id, &event.conversation_id)
|
||
.await;
|
||
|
||
// 2. 检查基础消息权限
|
||
if !role.has_permission(Permission::SEND_MESSAGE) {
|
||
return PermissionResult {
|
||
allowed: false,
|
||
reason: Some("用户被禁止发送消息".into()),
|
||
};
|
||
}
|
||
|
||
// 3. 检查媒体权限
|
||
if event.has_media && !role.has_permission(Permission::SEND_MEDIA) {
|
||
return PermissionResult {
|
||
allowed: false,
|
||
reason: Some("用户被禁止发送媒体".into()),
|
||
};
|
||
}
|
||
|
||
// 4. 检查命令权限
|
||
if event.is_command {
|
||
let cmd_perm = self
|
||
.command_permissions
|
||
.get(&event.command_name)
|
||
.copied()
|
||
.unwrap_or(Permission::EXECUTE);
|
||
|
||
if !role.has_permission(cmd_perm) {
|
||
return PermissionResult {
|
||
allowed: false,
|
||
reason: Some(format!("用户无权执行命令: {}", event.command_name)),
|
||
};
|
||
}
|
||
}
|
||
|
||
PermissionResult { allowed: true, reason: None }
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.5 命令权限配置
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
permissions:
|
||
# 默认角色权限
|
||
default_role: "member"
|
||
|
||
# 角色能力定义
|
||
roles:
|
||
owner:
|
||
capabilities: 0o700
|
||
inherits: ["admin"]
|
||
|
||
admin:
|
||
capabilities: 0o600
|
||
inherits: ["member"]
|
||
|
||
member:
|
||
capabilities: 0o400
|
||
inherits: ["guest"]
|
||
|
||
guest:
|
||
capabilities: 0o100
|
||
inherits: []
|
||
|
||
blocked:
|
||
capabilities: 0o000
|
||
inherits: []
|
||
|
||
# 斜杠命令权限
|
||
commands:
|
||
# 公开命令(所有人均可使用)
|
||
public:
|
||
- "/help"
|
||
- "/status"
|
||
- "/ping"
|
||
|
||
# 成员命令(member 及以上)
|
||
member:
|
||
- "/search"
|
||
- "/weather"
|
||
- "/translate"
|
||
|
||
# 管理员命令(admin 及以上)
|
||
admin:
|
||
- "/kick"
|
||
- "/ban"
|
||
- "/mute"
|
||
- "/warn"
|
||
- "/config"
|
||
|
||
# 拥有者命令(仅 owner)
|
||
owner:
|
||
- "/transfer"
|
||
- "/delete"
|
||
- "/backup"
|
||
- "/reload"
|
||
|
||
# 权限继承配置
|
||
inheritance:
|
||
enabled: true
|
||
max_depth: 5 # 最大继承深度,防止循环
|
||
```
|
||
|
||
### 7.6 用户角色管理
|
||
|
||
```rust
|
||
#[async_trait]
|
||
pub trait RoleManager: Send + Sync {
|
||
/// 获取用户在特定会话中的角色
|
||
async fn get_role(
|
||
&self,
|
||
user_id: &str,
|
||
conversation_id: &str,
|
||
) -> Role;
|
||
|
||
/// 设置用户角色(需要相应权限)
|
||
async fn set_role(
|
||
&self,
|
||
user_id: &str,
|
||
conversation_id: &str,
|
||
role: Role,
|
||
operator_id: &str,
|
||
) -> Result<(), PermissionDenied>;
|
||
|
||
/// 转让所有权
|
||
async fn transfer_ownership(
|
||
&self,
|
||
conversation_id: &str,
|
||
new_owner_id: &str,
|
||
) -> Result<(), PermissionDenied>;
|
||
}
|
||
|
||
pub struct SqliteRoleManager {
|
||
pool: SqlitePool,
|
||
}
|
||
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum PermissionDenied {
|
||
#[error("权限不足: {0}")]
|
||
Insufficient(String),
|
||
#[error("无法设置比自己更高的权限")]
|
||
CannotElevate,
|
||
}
|
||
|
||
#[async_trait]
|
||
impl RoleManager for SqliteRoleManager {
|
||
async fn get_role(
|
||
&self,
|
||
user_id: &str,
|
||
conversation_id: &str,
|
||
) -> Role {
|
||
// 1. 检查全局管理员
|
||
if self.is_global_admin(user_id).await {
|
||
return Role::Owner;
|
||
}
|
||
|
||
// 2. 检查会话特定角色
|
||
if let Some(role_data) = self.storage.get_user_role(user_id, conversation_id).await {
|
||
return Role::from_bits(role_data.role);
|
||
}
|
||
|
||
// 3. 返回默认角色
|
||
Role::Member
|
||
}
|
||
|
||
async fn set_role(
|
||
&self,
|
||
user_id: &str,
|
||
conversation_id: &str,
|
||
role: Role,
|
||
operator_id: &str,
|
||
) -> Result<(), PermissionDenied> {
|
||
let operator_role = self.get_role(operator_id, conversation_id).await;
|
||
|
||
// 检查操作者权限
|
||
if role.bits() > operator_role.bits() {
|
||
return Err(PermissionDenied::CannotElevate);
|
||
}
|
||
|
||
self.storage
|
||
.set_user_role(user_id, conversation_id, role.bits())
|
||
.await;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.7 会话级权限配置
|
||
|
||
```rust
|
||
#[derive(Debug, Clone)]
|
||
pub struct ConversationPermissions {
|
||
pub conversation_id: String,
|
||
|
||
// 基础权限
|
||
pub default_role: Role,
|
||
pub allow_guest_read: bool,
|
||
pub allow_guest_send: bool,
|
||
|
||
// 功能开关
|
||
pub allow_media: bool,
|
||
pub allow_commands: bool,
|
||
pub allow_ai_responses: bool,
|
||
|
||
// 限制
|
||
pub max_message_length: usize,
|
||
pub max_messages_per_minute: usize,
|
||
pub max_commands_per_minute: usize,
|
||
|
||
// 白名单/黑名单
|
||
pub whitelist: Vec<String>,
|
||
pub blacklist: Vec<String>,
|
||
}
|
||
|
||
impl ConversationPermissions {
|
||
/// 检查用户是否允许执行操作
|
||
pub fn check_user_allowed(&self, user_id: &str, permission: Permission) -> bool {
|
||
if self.blacklist.contains(&user_id.to_string()) {
|
||
return false;
|
||
}
|
||
|
||
if !self.whitelist.is_empty() && !self.whitelist.contains(&user_id.to_string()) {
|
||
return false;
|
||
}
|
||
|
||
true
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.8 权限事件
|
||
|
||
```rust
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum PermissionEvent {
|
||
RoleChanged,
|
||
PermissionDenied,
|
||
UserBanned,
|
||
UserUnbanned,
|
||
CommandBlocked,
|
||
OwnershipTransferred,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct PermissionAuditLog {
|
||
pub event: PermissionEvent,
|
||
pub operator_id: String,
|
||
pub target_id: String,
|
||
pub conversation_id: String,
|
||
pub details: HashMap<String, String>,
|
||
pub timestamp: i64,
|
||
}
|
||
```
|
||
|
||
### 7.9 与 Unix 的类比
|
||
|
||
```
|
||
┌─────────────────┬────────────────────────┐
|
||
│ Unix 概念 │ AstrBot 对应 │
|
||
├─────────────────┼────────────────────────┤
|
||
│ 用户 (User) │ 用户 (User) │
|
||
│ 用户组 (Group) │ 会话 (Conversation) │
|
||
│ root 用户 │ Owner (拥有者) │
|
||
│ sudo 用户 │ Admin (管理员) │
|
||
│ 普通用户 │ Member (成员) │
|
||
│ 访客 │ Guest (访客) │
|
||
│ 文件权限 rwx │ 能力 (Capability) │
|
||
│ chmod │ set_role │
|
||
│ chown │ transfer_ownership │
|
||
│ /etc/passwd │ Role Storage │
|
||
└─────────────────┴────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 输出缓冲区(Output Buffer)
|
||
|
||
### 8.1 队列结构
|
||
|
||
```python
|
||
@dataclass
|
||
class OutputMessage:
|
||
"""输出消息单元"""
|
||
session_id: str
|
||
content: str | AsyncIterator[str] # 支持流式
|
||
format: str = "plain" # plain | markdown | html
|
||
strategy: OutputStrategy = OutputStrategy.FULL
|
||
metadata: dict = field(default_factory=dict)
|
||
|
||
# 流式相关
|
||
stream_start_time: float | None = None
|
||
total_sent: int = 0
|
||
|
||
@dataclass
|
||
class ResultQueue:
|
||
"""结果队列"""
|
||
session_id: str
|
||
results: deque[OutputMessage]
|
||
max_size: int = 100
|
||
allow_streaming: bool = True
|
||
|
||
class OutputStrategy(Enum):
|
||
"""输出策略"""
|
||
STREAMING = "streaming" # 流式输出
|
||
SEGMENTED = "segmented" # 智能分段
|
||
FULL = "full" # 一次性输出
|
||
```
|
||
|
||
### 8.2 输出策略
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
output:
|
||
# 默认输出策略
|
||
default_strategy: "streaming" # streaming | segmented | full
|
||
|
||
# 流式配置
|
||
streaming:
|
||
# 启用流式
|
||
enable: true
|
||
|
||
# 流式 Chunk 大小(字符数)
|
||
chunk_size: 20
|
||
|
||
# Chunk 之间的间隔(秒)
|
||
chunk_interval: 0.05
|
||
|
||
# 智能分段配置
|
||
segmented:
|
||
# 启用智能分段
|
||
enable: true
|
||
|
||
# 触发分段的字数阈值
|
||
threshold: 500
|
||
|
||
# 分段方式
|
||
mode: "sentence" # sentence | word_count | regex
|
||
|
||
# 按句子分段时的最小长度
|
||
min_segment_length: 50
|
||
|
||
# 分段正则(当 mode=regex)
|
||
split_regex: "[。!?;\n]+"
|
||
|
||
# 段落之间的随机间隔(秒)
|
||
random_interval: "0.5,2.0"
|
||
|
||
# 是否在分段前添加省略号
|
||
add_ellipsis: true
|
||
|
||
# 平台适配
|
||
platform_adaptation:
|
||
# 平台与策略映射
|
||
strategy_by_platform:
|
||
telegram: "segmented" # Telegram 有字数限制
|
||
discord: "segmented" # Discord 也有限制
|
||
qq: "segmented"
|
||
webchat: "streaming" # WebChat 支持流式
|
||
|
||
# 平台消息长度限制
|
||
max_length_by_platform:
|
||
telegram: 4096
|
||
discord: 2000
|
||
qq: 500
|
||
|
||
# 输出缓冲配置
|
||
buffer:
|
||
# 最大缓冲消息数
|
||
max_size: 100
|
||
|
||
# 消息最大存活时间(秒)
|
||
max_age: 300
|
||
|
||
# 溢出策略
|
||
overflow_strategy: "drop_oldest"
|
||
```
|
||
|
||
### 8.3 分段器实现
|
||
|
||
```python
|
||
class SmartSegmenter:
|
||
"""智能分段器"""
|
||
|
||
def __init__(self, config: SegmentedConfig):
|
||
self.config = config
|
||
|
||
def segment(self, content: str) -> list[str]:
|
||
"""将内容分段"""
|
||
|
||
if len(content) < self.config.threshold:
|
||
return [content]
|
||
|
||
if self.config.mode == "sentence":
|
||
return self._split_by_sentence(content)
|
||
elif self.config.mode == "word_count":
|
||
return self._split_by_word_count(content)
|
||
elif self.config.mode == "regex":
|
||
return self._split_by_regex(content)
|
||
|
||
return [content]
|
||
|
||
def _split_by_sentence(self, content: str) -> list[str]:
|
||
"""按句子分段"""
|
||
sentences = re.split(
|
||
self.config.split_regex,
|
||
content,
|
||
)
|
||
|
||
segments = []
|
||
current = []
|
||
|
||
for sentence in sentences:
|
||
if not sentence.strip():
|
||
continue
|
||
|
||
current.append(sentence)
|
||
current_text = "".join(current)
|
||
|
||
# 如果当前段落达到阈值
|
||
if len(current_text) >= self.config.threshold:
|
||
segment = "".join(current)
|
||
if self.config.add_ellipsis and len(segments) > 0:
|
||
segment = "..." + segment.lstrip()
|
||
segments.append(segment)
|
||
current = []
|
||
|
||
# 处理剩余内容
|
||
if current:
|
||
remaining = "".join(current)
|
||
if remaining.strip():
|
||
if self.config.add_ellipsis and segments:
|
||
remaining = "..." + remaining.lstrip()
|
||
segments.append(remaining)
|
||
|
||
return segments
|
||
|
||
async def stream_segments(
|
||
self,
|
||
content: str,
|
||
output: OutputMessage,
|
||
sender: Callable[[str], Awaitable[None]],
|
||
) -> None:
|
||
"""流式发送分段"""
|
||
|
||
segments = self.segment(content)
|
||
|
||
for i, segment in enumerate(segments):
|
||
# 发送当前分段
|
||
await sender(segment)
|
||
|
||
# 添加间隔(随机)
|
||
if i < len(segments) - 1:
|
||
interval = self._random_interval()
|
||
await asyncio.sleep(interval)
|
||
|
||
def _random_interval(self) -> float:
|
||
"""生成随机间隔"""
|
||
import random
|
||
parts = self.config.random_interval.split(",")
|
||
return random.uniform(float(parts[0]), float(parts[1]))
|
||
```
|
||
|
||
### 8.4 流式输出器
|
||
|
||
```python
|
||
class StreamingOutput:
|
||
"""流式输出器"""
|
||
|
||
def __init__(self, config: StreamingConfig):
|
||
self.config = config
|
||
|
||
async def stream(
|
||
self,
|
||
content: str,
|
||
sender: Callable[[str], Awaitable[None]],
|
||
) -> None:
|
||
"""流式输出内容"""
|
||
|
||
start = 0
|
||
while start < len(content):
|
||
end = start + self.config.chunk_size
|
||
chunk = content[start:end]
|
||
|
||
await sender(chunk)
|
||
|
||
start = end
|
||
|
||
# 添加短暂间隔
|
||
if start < len(content):
|
||
await asyncio.sleep(self.config.chunk_interval)
|
||
|
||
def create_stream(
|
||
self,
|
||
content: str,
|
||
) -> AsyncIterator[str]:
|
||
"""创建流式迭代器"""
|
||
|
||
async def generator():
|
||
start = 0
|
||
while start < len(content):
|
||
end = start + self.config.chunk_size
|
||
chunk = content[start:end]
|
||
yield chunk
|
||
start = end
|
||
|
||
if start < len(content):
|
||
await asyncio.sleep(self.config.chunk_interval)
|
||
|
||
return generator()
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 记忆管理(Memory Management)
|
||
|
||
### 9.1 记忆存储配置
|
||
|
||
```yaml
|
||
# agent.yaml
|
||
memory:
|
||
# 记忆存储类型
|
||
backend: "sqlite" # sqlite | redis | memory
|
||
|
||
# SQLite 配置
|
||
sqlite:
|
||
path: "$XDG_DATA_HOME/astrbot/state/memory.db"
|
||
|
||
# Redis 配置
|
||
redis:
|
||
host: "localhost"
|
||
port: 6379
|
||
db: 0
|
||
prefix: "astrbot:memory:"
|
||
|
||
# 记忆保留策略
|
||
retention:
|
||
# 工作记忆:保留在数据库中的时间(天)
|
||
working_memory_days: 7
|
||
|
||
# 长期记忆:超过后转为归档
|
||
long_term_threshold_days: 30
|
||
|
||
# 自动摘要阈值(对话轮数)
|
||
auto_summary_threshold: 50
|
||
|
||
# 每次摘要保留的关键信息数
|
||
summary_keep_key_points: 5
|
||
|
||
# 上下文窗口内的记忆
|
||
context_window:
|
||
# 保留最近 N 轮对话的完整记忆
|
||
recent_rounds: 10
|
||
|
||
# 超出后转为摘要
|
||
summarize_beyond: true
|
||
```
|
||
|
||
### 9.2 记忆类型
|
||
|
||
```python
|
||
class MemoryType(Enum):
|
||
"""记忆类型"""
|
||
WORKING = "working" # 工作记忆(当前会话)
|
||
EPISODIC = "episodic" # 情景记忆(历史事件)
|
||
SEMANTIC = "semantic" # 语义记忆(持久知识)
|
||
|
||
@dataclass
|
||
class MemoryEntry:
|
||
"""记忆条目"""
|
||
id: str
|
||
type: MemoryType
|
||
content: str
|
||
embedding: list[float] | None = None
|
||
metadata: dict = field(default_factory=dict)
|
||
created_at: float
|
||
updated_at: float
|
||
access_count: int = 0
|
||
importance: float = 0.5 # 0-1 重要性评分
|
||
|
||
class MemoryBank:
|
||
"""记忆库"""
|
||
|
||
def __init__(self, config: MemoryConfig):
|
||
self.config = config
|
||
self.backend = self._create_backend(config)
|
||
self._cache: dict[str, MemoryEntry] = {}
|
||
self._cache_max_size = 100
|
||
|
||
async def add(self, message: Message) -> None:
|
||
"""添加记忆"""
|
||
entry = MemoryEntry(
|
||
id=str(uuid.uuid4()),
|
||
type=MemoryType.EPISODIC,
|
||
content=message.content,
|
||
metadata={
|
||
"role": message.role,
|
||
"user_id": message.metadata.get("user_id"),
|
||
"session_id": message.metadata.get("session_id"),
|
||
},
|
||
created_at=time.time(),
|
||
updated_at=time.time(),
|
||
)
|
||
|
||
await self.backend.save(entry)
|
||
|
||
async def search(
|
||
self,
|
||
query: str,
|
||
limit: int = 5,
|
||
memory_types: list[MemoryType] | None = None,
|
||
) -> list[MemoryEntry]:
|
||
"""搜索记忆"""
|
||
|
||
# 1. 如果有缓存,直接返回
|
||
cache_key = f"{query}:{limit}"
|
||
if cache_key in self._cache:
|
||
return self._cache[cache_key]
|
||
|
||
# 2. 向量搜索
|
||
results = await self.backend.search(
|
||
query=query,
|
||
limit=limit,
|
||
memory_types=memory_types,
|
||
)
|
||
|
||
# 3. 更新访问计数
|
||
for entry in results:
|
||
entry.access_count += 1
|
||
await self.backend.update(entry)
|
||
|
||
# 4. 缓存
|
||
if len(self._cache) >= self._cache_max_size:
|
||
# LRU 淘汰
|
||
oldest = min(self._cache.values(), key=lambda x: x.access_count)
|
||
del self._cache[oldest.id]
|
||
|
||
self._cache[cache_key] = results
|
||
|
||
return results
|
||
|
||
async def summarize_old(
|
||
self,
|
||
before_timestamp: float,
|
||
) -> str:
|
||
"""摘要旧记忆"""
|
||
|
||
# 1. 获取指定时间前的记忆
|
||
entries = await self.backend.get_before(before_timestamp)
|
||
|
||
if not entries:
|
||
return ""
|
||
|
||
# 2. 构建摘要
|
||
summary_prompt = f"""请简洁总结以下对话要点:
|
||
|
||
{chr(10).join(f"- {e.content}" for e in entries)}
|
||
|
||
保留关键信息:
|
||
- 主要话题或问题
|
||
- 已确定的结论或方案
|
||
- 未完成的任务
|
||
"""
|
||
|
||
# 3. 调用 LLM 摘要
|
||
summary = await self._llm_summarize(summary_prompt)
|
||
|
||
# 4. 创建摘要记忆
|
||
summary_entry = MemoryEntry(
|
||
id=str(uuid.uuid4()),
|
||
type=MemoryType.SEMANTIC,
|
||
content=summary,
|
||
metadata={"original_entries": len(entries)},
|
||
created_at=time.time(),
|
||
updated_at=time.time(),
|
||
importance=0.7,
|
||
)
|
||
|
||
await self.backend.save(summary_entry)
|
||
|
||
# 5. 删除原始记忆
|
||
for entry in entries:
|
||
await self.backend.delete(entry.id)
|
||
|
||
return summary
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 平台适配(Platform Adaptation)
|
||
|
||
### 10.1 平台特性
|
||
|
||
```python
|
||
@dataclass
|
||
class PlatformCapabilities:
|
||
"""平台能力"""
|
||
supports_streaming: bool = False
|
||
max_message_length: int = 4096
|
||
supports_markdown: bool = True
|
||
supports_html: bool = False
|
||
supports_images: bool = True
|
||
supports_mentions: bool = True
|
||
supports_reply: bool = True
|
||
rate_limit_rpm: int = 60
|
||
rate_limit_rpd: int = 10000
|
||
|
||
PLATFORM_CAPABILITIES = {
|
||
"telegram": PlatformCapabilities(
|
||
supports_streaming=False,
|
||
max_message_length=4096,
|
||
supports_markdown=True,
|
||
supports_html=True,
|
||
),
|
||
"discord": PlatformCapabilities(
|
||
supports_streaming=False,
|
||
max_message_length=2000,
|
||
supports_markdown=True,
|
||
supports_html=False,
|
||
supports_reply=True,
|
||
),
|
||
"qq": PlatformCapabilities(
|
||
supports_streaming=False,
|
||
max_message_length=500,
|
||
supports_markdown=False,
|
||
supports_mentions=True,
|
||
),
|
||
"webchat": PlatformCapabilities(
|
||
supports_streaming=True,
|
||
max_message_length=10000,
|
||
supports_markdown=True,
|
||
supports_html=True,
|
||
),
|
||
}
|
||
```
|
||
|
||
### 10.2 策略选择器
|
||
|
||
```python
|
||
class PlatformStrategySelector:
|
||
"""平台策略选择器"""
|
||
|
||
def __init__(self, config: PlatformAdaptationConfig):
|
||
self.config = config
|
||
self.capabilities = PLATFORM_CAPABILITIES
|
||
|
||
def select_strategy(
|
||
self,
|
||
platform: str,
|
||
content_length: int,
|
||
user_preference: str | None = None,
|
||
) -> OutputStrategy:
|
||
"""选择输出策略"""
|
||
|
||
caps = self.capabilities.get(platform)
|
||
|
||
# 1. 用户偏好优先
|
||
if user_preference and self._is_valid_strategy(user_preference, caps):
|
||
return OutputStrategy(user_preference)
|
||
|
||
# 2. 平台能力判断
|
||
if not caps:
|
||
return OutputStrategy.FULL
|
||
|
||
# 3. 平台配置覆盖
|
||
platform_strategy = self.config.strategy_by_platform.get(platform)
|
||
if platform_strategy:
|
||
return OutputStrategy(platform_strategy)
|
||
|
||
# 4. 内容长度判断
|
||
if content_length > caps.max_message_length:
|
||
return OutputStrategy.SEGMENTED
|
||
|
||
# 5. 流式支持判断
|
||
if caps.supports_streaming:
|
||
return OutputStrategy.STREAMING
|
||
|
||
return OutputStrategy.FULL
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 配置汇总
|
||
|
||
### 11.1 agent.yaml 完整配置
|
||
|
||
```yaml
|
||
# Agent 配置
|
||
|
||
# 输入缓冲区
|
||
input_buffer:
|
||
max_queue_size: 1000
|
||
max_message_age: 3600
|
||
overflow_strategy: "drop_oldest"
|
||
overflow_hint: "[消息过多,部分早期消息已丢弃]"
|
||
|
||
# 流控
|
||
flow_control:
|
||
mode: "auto"
|
||
auto:
|
||
api_rpm_limit: 60
|
||
messages_per_request: 5
|
||
safety_margin: 0.8
|
||
min_interval: 0.5
|
||
max_interval: 10
|
||
|
||
# 上下文
|
||
context:
|
||
max_context_tokens: 128000
|
||
compress_threshold: 0.85
|
||
keep_recent_messages: 6
|
||
compress_instruction: |
|
||
请简洁地总结对话要点...
|
||
|
||
# 工具调用
|
||
tool_calling:
|
||
strategy: "smart"
|
||
max_calls_per_request: 128
|
||
timeout: 60
|
||
max_retries: 3
|
||
parallel_calls: true
|
||
max_parallel_calls: 5
|
||
|
||
# 安全
|
||
security:
|
||
injection:
|
||
enable: true
|
||
mode: "strict"
|
||
patterns: [...]
|
||
on_detect: "sanitize"
|
||
content_filter:
|
||
enable: true
|
||
level: "standard"
|
||
replacement: "[已过滤]"
|
||
leakage_prevention:
|
||
blocked_file_patterns: [...]
|
||
blocked_output_patterns: [...]
|
||
placeholder: "[REDACTED]"
|
||
|
||
# 输出
|
||
output:
|
||
default_strategy: "streaming"
|
||
streaming:
|
||
chunk_size: 20
|
||
chunk_interval: 0.05
|
||
segmented:
|
||
enable: true
|
||
threshold: 500
|
||
mode: "sentence"
|
||
split_regex: "[。!?;\n]+"
|
||
random_interval: "0.5,2.0"
|
||
add_ellipsis: true
|
||
platform_adaptation:
|
||
strategy_by_platform:
|
||
telegram: "segmented"
|
||
discord: "segmented"
|
||
webchat: "streaming"
|
||
max_length_by_platform:
|
||
telegram: 4096
|
||
discord: 2000
|
||
|
||
# 记忆
|
||
memory:
|
||
backend: "sqlite"
|
||
sqlite:
|
||
path: "$XDG_DATA_HOME/astrbot/state/memory.db"
|
||
retention:
|
||
working_memory_days: 7
|
||
auto_summary_threshold: 50
|
||
context_window:
|
||
recent_rounds: 10
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 错误处理与恢复
|
||
|
||
### 12.1 错误分类
|
||
|
||
```python
|
||
class ErrorType(Enum):
|
||
"""错误类型"""
|
||
RATE_LIMIT = "rate_limit" # 限流
|
||
TIMEOUT = "timeout" # 超时
|
||
NETWORK = "network" # 网络错误
|
||
API = "api" # API 错误
|
||
TOOL = "tool" # 工具错误
|
||
SECURITY = "security" # 安全错误
|
||
INTERNAL = "internal" # 内部错误
|
||
|
||
@dataclass
|
||
class ErrorRecoveryConfig:
|
||
"""错误恢复配置"""
|
||
max_retries: dict[ErrorType, int] = field(default_factory=lambda: {
|
||
ErrorType.RATE_LIMIT: 5,
|
||
ErrorType.TIMEOUT: 3,
|
||
ErrorType.NETWORK: 3,
|
||
ErrorType.API: 2,
|
||
ErrorType.TOOL: 2,
|
||
ErrorType.SECURITY: 0,
|
||
ErrorType.INTERNAL: 1,
|
||
})
|
||
|
||
backoff_multiplier: float = 1.5
|
||
max_backoff: float = 60.0
|
||
```
|
||
|
||
### 12.2 错误处理策略
|
||
|
||
```python
|
||
async def handle_error(
|
||
error: Exception,
|
||
context: AgentContext,
|
||
config: ErrorRecoveryConfig,
|
||
) -> ErrorAction:
|
||
"""处理错误并决定下一步行动"""
|
||
|
||
error_type = classify_error(error)
|
||
retries = context.metadata.get(f"retry_{error_type.value}", 0)
|
||
|
||
if retries >= config.max_retries.get(error_type, 0):
|
||
return ErrorAction.FAIL
|
||
|
||
# 指数退避
|
||
if retries > 0:
|
||
backoff = min(
|
||
config.backoff_multiplier ** retries,
|
||
config.max_backoff
|
||
)
|
||
await asyncio.sleep(backoff)
|
||
|
||
context.metadata[f"retry_{error_type.value}"] = retries + 1
|
||
|
||
if error_type == ErrorType.RATE_LIMIT:
|
||
# 更新流控配置
|
||
flow_control.decrease_rate(0.8)
|
||
return ErrorAction.RETRY
|
||
|
||
elif error_type == ErrorType.SECURITY:
|
||
# 安全错误不重试
|
||
return ErrorAction.BLOCK
|
||
|
||
elif error_type == ErrorType.API:
|
||
# API 错误,检查是否可恢复
|
||
if is_retryable_api_error(error):
|
||
return ErrorAction.RETRY
|
||
return ErrorAction.FAIL
|
||
|
||
return ErrorAction.RETRY
|
||
|
||
class ErrorAction(Enum):
|
||
"""错误处理动作"""
|
||
RETRY = "retry"
|
||
FAIL = "fail"
|
||
BLOCK = "block"
|
||
FALLBACK = "fallback"
|
||
```
|
||
|
||
---
|
||
|
||
## 13. 扩展点
|
||
|
||
### 13.1 插件扩展点
|
||
|
||
```python
|
||
# 输入处理扩展
|
||
class InputBufferPlugin(ABC):
|
||
"""输入缓冲区插件"""
|
||
|
||
async def pre_add_message(
|
||
self,
|
||
message: InputMessage,
|
||
) -> InputMessage | None:
|
||
"""消息添加前拦截,返回 None 表示跳过"""
|
||
pass
|
||
|
||
async def post_add_message(
|
||
self,
|
||
message: InputMessage,
|
||
) -> None:
|
||
"""消息添加后处理"""
|
||
pass
|
||
|
||
# 输出处理扩展
|
||
class OutputBufferPlugin(ABC):
|
||
"""输出缓冲区插件"""
|
||
|
||
async def pre_send_message(
|
||
self,
|
||
message: OutputMessage,
|
||
) -> OutputMessage | None:
|
||
"""消息发送前拦截"""
|
||
pass
|
||
|
||
async def post_send_message(
|
||
self,
|
||
message: OutputMessage,
|
||
) -> None:
|
||
"""消息发送后处理"""
|
||
pass
|
||
|
||
# 安全扩展
|
||
class SecurityPlugin(ABC):
|
||
"""安全插件"""
|
||
|
||
async def check_injection(
|
||
self,
|
||
content: str,
|
||
) -> list[SecurityIssue]:
|
||
"""自定义注入检测"""
|
||
pass
|
||
|
||
async def filter_content(
|
||
self,
|
||
content: str,
|
||
) -> str:
|
||
"""自定义内容过滤"""
|
||
pass
|
||
```
|
||
|
||
### 13.2 调度器扩展
|
||
|
||
```python
|
||
# 自定义调度策略
|
||
class CustomScheduler(ABC):
|
||
"""自定义调度策略"""
|
||
|
||
async def select_next_message(
|
||
self,
|
||
queues: dict[str, UserMessageQueue],
|
||
) -> InputMessage | None:
|
||
"""选择下一条消息"""
|
||
pass
|
||
|
||
async def on_queue_empty(
|
||
self,
|
||
user_id: str,
|
||
) -> None:
|
||
"""队列为空时的处理"""
|
||
pass
|
||
```
|