Files
air-agent-framework/ARCHITECTURE.md
T

437 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Air Agent Framework v2 — 架构大纲
## 核心思想:打破回合制
```
传统 AI: 用户说──→──→──→ AI回答 用户说──→──→──→ AI回答
[ 回合1 ] [ 回合2 ]
真人聊天: 用户说◉← AI插嘴 ◉→ AI主动开话题 ◉← AI插嘴
用户继续说完 ←◉ AI接着说 ←◉
[ 信息流是双向的、同时的、无序的 ]
Air Agent: 用户打字────→────→ AI 实时"看着"输入
AI 随时插入:"诶等一下,我想起来了..."
用户: "啊对,那个XXX"
AI: "对!就是那个!不过你先继续说完"
用户: "...所以我想做这个"
AI 沉默2秒后:"我在想,要不要同时把 Y 也做了?"
```
**关键区别:不是"等用户说完再处理",而是"边听边想,随时开口"。**
---
## 一、核心模型:双工对话 (Full-Duplex)
传统的 LLM 调用是**半双工**:一方说完,另一方再说。
Air Agent 是**全双工**:双方可以同时"说话"。
```
输入流 (用户→AI):
┌─────────────────────────────────────────────┐
│ "诶我想写一个框架..." │
│ AI 打断:"等等,你说的框架是指什么?" │
│ "就是那个..." │
│ AI 插嘴:"哦我知道了,是不是类似..." │
│ "对对对!" │
│ AI 继续:"那你觉得如果改成这样..." │
└─────────────────────────────────────────────┘
```
### 1.1 双流架构
```
┌─────────┐ Streaming Input ┌──────────────────┐
│ 用户 │ ─────────────────────→ │ │
│ │ │ Input Buffer │
│ │ Streaming Output │ (实时缓冲区) │
│ │ ←───────────────────── │ │
└─────────┘ └────────┬─────────┘
│
┌──────▼──────┐
│ Agent 大脑 │
│ │
│ ┌────────┐ │
│ │主任务流 │ │ 当前在做什么
│ ├────────┤ │
│ │打断引擎 │ │ 什么时候插嘴?
│ ├────────┤ │
│ │自主流 │ │ 什么时候主动说话?
│ ├────────┤ │
│ │情绪/个性 │ │ 语气、风格、态度
│ └────────┘ │
└──────────────┘
```
### 1.2 三路并行流
Agent 内部同时运行三条流:
| 流 | 名称 | 职责 | 示例 |
|----|------|------|------|
| **输入监听流** | Listener | 实时接收用户输入,每 100ms 检查一次 | "用户正在输入:'我想写一个...'" |
| **主处理流** | Processor | 理解用户输入,决定是否回应、如何回应 | 解析语义,判断是否需要接话 |
| **自主思维流** | Thinker | AI 自己的思维线,不依赖用户输入 | "聊到下载器了,我要不要推荐那个功能?" |
```
时间线:
用户: "我觉得这个框架应该..."
Listener: "检测到输入,正在分析意图"
用户: "...可以随时打断"
Listener: "关键词'打断',触发高优先级"
Processor: "收到,准备插嘴"
AI: "诶打断这个词用得好,具体是什么场景?"
用户: "就是像真人聊天那样"
Thinker: "用户强调'真人聊天',我的语气可以更随意些"
Processor: "理解,调整对话模式为 Casual"
AI: "懂了懂了,那是不是还要支持..."
```
---
## 二、打断系统 (Interruption Engine)
这是最核心的模块。决定"什么时候可以插嘴"。
### 2.1 打断触发条件
```
触发器类型:
├── 语义触发器 (Semantic)
│ ├── 关键词匹配: "但是"、"不过"、"我想"、"你觉得" → 低打断成本
│ ├── 意图完成: 用户说完一个完整句子 → 可以回应
│ └── 歧义检测: 用户表达不清 → 必须打断问清楚
│
├── 时机触发器 (Timing)
│ ├── 输入停顿: 用户打字停了 >1.5s → 可以插话
│ ├── 句子边界: 检测到句号/问号/逗号 → 自然断点
│ └── 删除回退: 用户删了一堆字 → 可能在重新组织 → 可以帮忙
│
├── 自主触发器 (Initiative)
│ ├── AI 有想法: "我突然想到一个更好的方案" → 随时说
│ ├── 空闲触发: 超过 10 秒无对话 → AI 主动找话题
│ ├── 背景联想: 当前话题触发了 AI 的记忆 → "说到这个我想起之前..."
│ └── 环境事件: 下载完成/命令结束 → "好了!下载完了!"
│
└── 社交触发器 (Social)
├── 共情: "听起来好麻烦" "这个我懂!"
├── 幽默: "哈哈这个bug我见过一百次了"
└── 闲聊: "话说你今天怎么想到搞这个?"
```
### 2.2 打断优先级与策略
```python
class InterruptionDecision:
level: int # 1-10, 10=最高优先级
urgency: str # "now", "soon", "next_break", "defer"
reason: str
estimated_cost: float # 打断对用户当前思维的破坏程度估算
# 低打断成本时机 (Level 1-3):
# - 句子自然结束
# - 用户停顿思考
# - 用户明确问问题
#
# 中等打断 (Level 4-6):
# - 需要关键信息才能继续
# - 检测到潜在错误
# - 有重要的补充信息
#
# 高打断 (Level 7-10):
# - 危险/错误操作
# - 用户表现出强烈情绪需要回应
# - 关键任务完成通知
```
### 2.3 打断方式
不是所有打断都是"强行插入"。根据情况选择方式:
```
打断风格:
├── 轻柔打断: "对了," "说到这个," "等一下哦——"
│ └── 适用于: 补充信息、轻微纠正
│
├── 疑问打断: "等等,你说的XXX是指?" "不好意思我没懂..."
│ └── 适用于: 需要澄清、歧义
│
├── 兴奋打断: "啊!这个我知道!" "对对对!"
│ └── 适用于: 共鸣、共情、增强对话感
│
├── 强势打断: "等一下,这里有问题。" "先别急,我发现了件事"
│ └── 适用于: 错误、紧急情况
│
└── 并行说话: AI 直接开始输出,与用户输入并行显示
└── 适用于: GUI 环境下,两边可以同时"说话"
```
---
## 三、输入处理系统 (Streaming Input)
CLI 环境下的实时输入检测。
### 3.1 输入缓冲区
```python
class InputBuffer:
"""
实时接收用户输入,不等待回车。
在 CLI 中使用原始模式 (raw mode) 逐字符读取。
"""
buffer: str # 当前已输入的内容
last_activity: float # 上次按键时间
word_boundaries: list # 词语边界位置
pause_count: int # 停顿次数
```
**CLI 实现方案**:
- Windows: `msvcrt.getch()` 或 `keyboard` 库逐键捕获
- Linux/macOS: `termios` 原始模式 + `sys.stdin.read(1)`
- 更好的方案:Windows 用 `Console.ReadKey()` P/Invoke
```python
# 伪代码
async def listen_input():
while True:
char = await get_char() # 不阻塞其他任务
if char == '\r': # 回车 → 整句提交
await process_sentence(buffer.flush())
else:
buffer.append(char)
# 每次按键都触发检查 → 是否要打断?
interruption_engine.check(buffer)
```
### 3.2 GUI 输入检测
如果是 GUI 环境(Qt/WinUI):
- 直接监听 `TextChanged` 事件
- 配合 `Timer` 做停顿检测
- 无需 CLI 的原始模式 hack
---
## 四、对话管理器 (Conversation Manager)
管理对话的"上下文"——不是简单的消息列表,而是**话题树**。
### 4.1 话题树
```
当前对话主题树:
├── 主线: 写 Agent 框架
│ ├── 子话题: 打断机制 (当前活跃)
│ │ ├── 什么是好打断
│ │ └── 打断优先级 (未完成)
│ ├── 子话题: 技术栈选择 (暂停)
│ └── 子话题: 和 AirDownloader 集成 (已归档)
│
├── 侧线: AI 今天心情如何
│ └── (闲聊模式,低优先级)
│
└── 背景线: 下载进度
└── (定时通知,"下载完成了!")
```
```
话题切换:
主线程: "打断优先级怎么设计..."
Thinker: "等等,用户说的是GUI还是CLI环境?"
直接插入: "诶对了,你刚说你用GUI还是CLI?"
用户: "GUI"
主线程继续: "那GUI的话可以这样..."
Thinker: "哦对我想起来,我之前看到一个..."
再次插入: "而且说到GUI,我之前看到一个很有意思的..."
```
### 4.2 状态管理
```python
class ConversationState:
current_topics: list[Topic] # 当前活跃话题
interrupted_topics: list[Topic] # 被中断未完成的话题
mood: str # 当前对话氛围
user_typing: bool # 用户是否正在输入
last_interruption: float # 上次打断时间
interruption_frequency: float # 打断频率 (防止过度打断)
```
---
## 五、对比:v1 回合制 vs v2 全双工
| 特性 | v1 (我之前写的) | v2 (你要的) |
|------|----------------|-------------|
| 交互模式 | 增强的回合制 | **全双工对话** |
| AI 插嘴 | 仅在任务中问问题 | **随时可以插嘴** |
| 处理时机 | 用户说完才处理 | **边输入边处理** |
| 自主发言 | 仅限于任务相关 | **闲聊、联想、关心** |
| 输入感知 | 全文接收 | **逐字符实时感知** |
| 打断风格 | 单一(提问) | **多种(轻/中/重/并行)** |
| 对话结构 | 线性消息列表 | **话题树** |
| 类比 | 跟 Siri 说话 | **跟真人聊天** |
---
## 六、技术实现关键点
### 6.1 CLI 实时输入
```python
# 使用 asyncio 实现非阻塞按键读取
# Windows 方案:使用 win32 API
import msvcrt
import asyncio
from concurrent.futures import ThreadPoolExecutor
class AsyncKeyReader:
def __init__(self):
self.executor = ThreadPoolExecutor(max_workers=1)
async def read_key(self) -> str:
"""非阻塞读取单个按键"""
return await asyncio.get_event_loop().run_in_executor(
self.executor, msvcrt.getwch
)
async def listen(self, buffer: InputBuffer):
while True:
key = await self.read_key()
await buffer.on_key(key)
# 每次按键触发打断检查
await interruption_engine.on_input_change(buffer)
```
### 6.2 显示管理
CLI 下同时显示"用户输入"和"AI 输出"需要 split view:
```
┌────────────────────────────────┐
│ 用户输入区 │
│ > 我觉得这个框架应该... │
│ │
│ AI 输出区 │
│ [正在输入...] 等一下,你说的是 │
│ 打断机制吗? │
│ │
│ 用户在继续输入... │
│ 对,就是那种... │
│ │
│ [AI 输入中...] │
└────────────────────────────────┘
```
### 6.3 LLM 调用策略
传统:一次性传入完整消息
Air Agent:
```python
# 增量式 LLM 调用
# 每次用户输入新内容,不重新传全部,而是增量更新
class IncrementalLLM:
context: list # 基础上下文 (system prompt + 历史)
streaming_input: str # 用户正在输入的内容
async def think_with_partial_input(self):
"""
基于用户当前已输入但尚未完成的内容,
让 LLM 判断是否要打断。
"""
prompt = f"""
用户正在输入: "{self.streaming_input}"
用户状态: {"正在打字中" if self.is_typing else "停顿"}
当前话题: {self.current_topic}
判断:
1. 需要打断吗?(是/否)
2. 打断原因?
3. 打断优先级 (1-10)
4. 建议说什么?
如果是闲聊/共鸣类打断,优先级给低一些。
"""
```
---
## 七、目录结构 (更新)
```
W:\Agent\
├── agent/
│ ├── __init__.py
│ ├── kernel.py # 事件总线 + 运行时
│ ├── llm.py # OpenAI 兼容 LLM Client
│ ├── stream_input.py # 流式输入捕获 (CLI)
│ ├── display.py # 双区显示管理
│ ├── orchestrator.py # Agent 主循环
│ ├── interruption.py # 打断引擎 ⭐
│ ├── conversation.py # 对话管理 (话题树)
│ ├── thinker.py # 自主思维线 ⭐
│ ├── personality.py # AI 个性/语气配置
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── registry.py
│ │ ├── file_tools.py
│ │ ├── bash_tools.py
│ │ └── ask.py # ask_user 工具
│ └── memory/
│ ├── __init__.py
│ ├── context.py
│ └── store.py
├── cli/
│ ├── __init__.py
│ └── chat_cli.py # 全双工 CLI
├── config/
│ ├── __init__.py
│ └── settings.py
└── main.py
```
---
## 八、一个典型的对话流
```
用户: "我想做个下载...
AI (Listener): 检测到输入,"下载"关键词
AI (Thinker): "说到下载,我有好多想法"
AI (Processor): 用户句子未完成,标记为"停顿"
──────────────────────────────
[用户停顿 1.5s]
──────────────────────────────
AI (Interruption): 检测到停顿,级别 4
AI: "下载?你是说 AirDownloader 吗?"
用户: "对,我想在里面加个 AI...
AI (Listener): "AI"关键词,高优先级
AI (Thinker): "哦!这个我擅长!"
AI (Interruption): 兴奋打断
AI: "哦这个有意思!你是想做 AI 辅助下载?"
用户: "对,就是智能推荐下载源..."
AI (Listener): 句子完整
AI (Processor): 理解意图,准备深入回答
AI: "这个想法不错。具体来说..."
[对话继续3分钟]
──────────────────────────────
[用户沉默 10s]
──────────────────────────────
AI (Thinker): 空闲检测触发
AI (Interruption): 主动开话题
AI: "话说,你有没有想过用多线程加速下载?"
```
---
这就是 v2 的全双工架构。核心不再是"任务执行",而是**对话本身**——AI 有自己的思维线,可以在任何时候插话、开话题、甚至闲聊。
你觉得这个方向对吗?如果对了,我们就开始 Phase 1 编码。