16 KiB
16 KiB
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 打断优先级与策略
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 输入缓冲区
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
# 伪代码
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 状态管理
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 实时输入
# 使用 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:
# 增量式 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 编码。