Files
air-agent-framework/ARCHITECTURE.md
T

16 KiB
Raw Blame History

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 编码。