437 lines
16 KiB
Markdown
437 lines
16 KiB
Markdown
# 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 编码。
|