返回博客列表
技术教程2026-03-2530 分钟阅读

CFTA异步调用链优化:从阻塞15秒到非阻塞并发

从阻塞 15 秒到非阻塞并发:工具调用链的 asyncio 全链路改造。

WeClaw_38_CFTA异步调用链优化:从阻塞15秒到非阻塞并发

作者: WeClaw 开发团队
日期: 2026-03-25
版本: v1.0
标签: CFTA、异步编程、并发控制、事件总线、Tool Calls、用户体验优化


📖 摘要

本文深入剖析 WeClaw CFTA(Chat-First, Tools-Async)异步调用链的完整设计与优化过程。针对 AI 对话系统中"工具调用阻塞响应"这一核心痛点,我们展示了如何通过异步解耦、事件驱动、Fire-and-Forget 等技术手段,将原本 15 秒的阻塞等待优化为即时响应。文章涵盖 CFTA 架构原理、事件总线设计、后台任务管理、流式 TTS 协调等核心技术实践。

核心收获

  • 🔥 理解 CFTA 架构的核心思想
  • 📡 掌握事件驱动编程范式
  • ⚡ 学会异步任务管理与取消机制
  • 🎯 理解流式响应与后台任务的协调
  • 🚀 获得响应延迟优化的实战经验

🎯 需求背景:为什么需要 CFTA?

传统 AI 对话的痛点

在传统 AI Agent 系统中,当用户询问需要执行工具的问题时:

用户: "帮我搜索最近的天气"
    ↓
AI 模型思考 + 生成 tool_call (约 2 秒)
    ↓
【阻塞等待】工具执行 (约 10-15 秒)
    ↓
AI 模型基于结果生成回复 (约 3 秒)
    ↓
最终响应 (总耗时 15-20 秒)

用户体验问题

  • ❌ 等待时间长(15-20 秒)
  • ❌ 无法进行其他操作
  • ❌ UI 可能卡死
  • ❌ TTS 语音播报延迟
  • ❌ 用户不确定是否在工作

CFTA 的解决思路

核心理念:Chat-First(聊天优先),Tools-Async(工具异步)

用户: "帮我搜索最近的天气"
    ↓
AI 模型思考 + 生成 tool_call (约 2 秒)
    ↓
【立即响应】"好的,我正在帮你查询天气..." (2 秒)
    ↓
【后台执行】工具异步执行 (10-15 秒)
    ↓
工具完成后 → UI 日志记录 (不重复显示)

用户体验提升

  • ✅ 快速响应(2 秒 vs 15 秒)
  • ✅ 可以继续交互
  • ✅ UI 流畅
  • ✅ 工具执行透明可见

🏗️ 整体架构设计

CFTA 架构图

┌─────────────────────────────────────────────────────┐
│                     UI 层                           │
│  - 聊天窗口(消息展示)                             │
│  - 工具日志面板                                    │
│  - 流式 TTS 播放器                                 │
└───────────────────┬─────────────────────────────────┘
                    │
        ┌───────────▼───────────┐
        │     Event Bus         │
        │  (事件总线)            │
        │  - tool_call          │
        │  - tool_result        │
        │  - deferred_started   │
        │  - deferred_result    │
        └───────────┬───────────┘
                    │
┌───────────────────┴─────────────────────────────────┐
│                   Agent 层                          │
│                                                      │
│  ┌─────────────────┐    ┌─────────────────┐        │
│  │  chat_stream    │    │ process_deferred │        │
│  │  (快速聊天)      │    │  (异步工具)      │        │
│  └────────┬────────┘    └────────┬────────┘        │
│           │                      │                  │
│           │    chat_lock         │                  │
│           └──────────┬───────────┘                  │
│                      │                              │
│  ┌───────────────────┴───────────────────┐          │
│  │         Tool Registry               │          │
│  │  (工具注册与执行)                     │          │
│  └─────────────────────────────────────┘          │
└─────────────────────────────────────────────────────┘

核心组件职责

组件职责关键技术
Event Bus事件发布/订阅观察者模式
chat_stream快速聊天流式响应asyncio
process_deferred后台异步工具执行asyncio.Task
Tool Registry工具发现与调用反射/动态导入
Session Manager会话历史管理SQLite

📡 核心模块一:事件总线设计

事件类型定义

class EventType:
    """事件类型枚举。"""
    
    # 模型调用
    MODEL_CALL = "model_call"
    MODEL_RESPONSE = "model_response"
    MODEL_REASONING = "model_reasoning"
    MODEL_USAGE = "model_usage"
    
    # 工具调用
    TOOL_CALL = "tool_call"
    TOOL_RESULT = "tool_result"
    
    # CFTA: 异步工具执行
    DEFERRED_TOOL_STARTED = "deferred_tool_started"
    DEFERRED_TOOL_RESULT = "deferred_tool_result"


@dataclass
class ToolCallEvent:
    """工具调用事件数据。"""
    tool_name: str
    action_name: str
    arguments: dict[str, Any]
    function_name: str  # 完整函数名
    session_id: str = ""


@dataclass
class ToolResultEvent:
    """工具结果事件数据。"""
    tool_name: str
    action_name: str
    status: str
    output: str
    error: Optional[str]
    duration_ms: float
    session_id: str = ""


@dataclass
class DeferredToolStartedEvent:
    """CFTA: 后台异步工具开始事件。"""
    user_input: str
    session_id: str = ""


@dataclass
class DeferredToolResultEvent:
    """CFTA: 后台异步工具结果事件。"""
    summary: str
    session_id: str = ""

事件总线实现

from typing import Callable, Any
from dataclasses import dataclass
import asyncio


class EventBus:
    """轻量级事件总线。
    
    支持:
    - 同步/异步订阅
    - 事件过滤
    - 错误隔离
    """
    
    def __init__(self):
        self._subscribers: dict[str, list[tuple[str, Callable]]] = {}
        self._locks: dict[str, asyncio.Lock] = {}
    
    def on(self, event_type: str, callback: Callable, filter_func: Callable = None) -> str:
        """订阅事件。
        
        Args:
            event_type: 事件类型
            callback: 回调函数
            filter_func: 过滤函数(可选)
            
        Returns:
            订阅 ID,用于取消订阅
        """
        if event_type not in self._subscribers:
            self._subscribers[event_type] = []
            self._locks[event_type] = asyncio.Lock()
        
        # 生成订阅 ID
        import uuid
        sub_id = str(uuid.uuid4())[:8]
        
        self._subscribers[event_type].append((sub_id, callback, filter_func))
        
        return sub_id
    
    def off(self, event_type: str, sub_id: str) -> None:
        """取消订阅。"""
        if event_type not in self._subscribers:
            return
        
        self._subscribers[event_type] = [
            (sid, cb, filt) for sid, cb, filt in self._subscribers[event_type]
            if sid != sub_id
        ]
    
    async def emit(self, event_type: str, event_data: Any) -> None:
        """发布事件。
        
        Args:
            event_type: 事件类型
            event_data: 事件数据
        """
        if event_type not in self._subscribers:
            return
        
        subscribers = self._subscribers[event_type]
        
        for sub_id, callback, filter_func in subscribers:
            # 过滤检查
            if filter_func and not filter_func(event_data):
                continue
            
            # 执行回调
            try:
                if asyncio.iscoroutinefunction(callback):
                    await callback(event_data)
                else:
                    callback(event_data)
            except Exception as e:
                logger.error(f"事件回调执行失败:{event_type}.{sub_id}: {e}")
    
    def once(self, event_type: str, callback: Callable) -> str:
        """订阅一次性事件(触发后自动取消)。"""
        sub_id = None
        
        async def wrapper(data):
            nonlocal sub_id
            self.off(event_type, sub_id)
            if asyncio.iscoroutinefunction(callback):
                await callback(data)
            else:
                callback(data)
        
        sub_id = self.on(event_type, wrapper)
        return sub_id

事件订阅示例

# 在 UI 层订阅工具调用事件
async def _on_tool_call(data: ToolCallEvent):
    """工具调用时更新 UI。"""
    self.tool_status_label.setText(f"正在调用:{data.tool_name}")
    self.tool_log.append(f"🔧 {data.tool_name}.{data.action_name}")

async def _on_tool_result(data: ToolResultEvent):
    """工具结果时更新 UI。"""
    if data.status == "success":
        self.tool_log.append(f"✅ {data.tool_name}: 成功 ({data.duration_ms:.0f}ms)")
    else:
        self.tool_log.append(f"❌ {data.tool_name}: {data.error}")

# 订阅事件
sub_tc = event_bus.on("tool_call", _on_tool_call)
sub_tr = event_bus.on("tool_result", _on_tool_result)

# 取消订阅
event_bus.off("tool_call", sub_tc)

⚡ 核心模块二:chat_stream 快速聊天

快速聊天实现

async def chat_stream_voice_fast(self, user_input: str) -> AsyncGenerator[str, None]:
    """语音模式快速聊天流(无工具),最小化首 token 延迟。
    
    差异点与标准 chat_stream:
    - 不做工具暴露,不传 tools 给模型
    - 使用轻量 system prompt(核心 + 陪伴模块)
    - 仅做流式文本输出,不支持 tool_calls
    - 单次模型调用,不进入 ReAct 循环
    """
    await self.chat_lock.acquire()
    try:
        async for chunk in self._chat_stream_voice_fast_impl(user_input):
            yield chunk
    finally:
        self.chat_lock.release()


async def _chat_stream_voice_fast_impl(
    self, user_input: str
) -> AsyncGenerator[str, None]:
    """快速聊天内部实现(已持有 chat_lock)。"""
    session = self.session_manager.current_session
    session_id = session.id
    
    # 【关键修复】清理未完成的 tool_calls
    cleaned = self.session_manager.cleanup_incomplete_tool_calls()
    if cleaned > 0:
        logger.warning("[CFTA-fast] 已补全 %d 条缺失的 tool 响应消息", cleaned)
    
    # 添加用户消息
    self.session_manager.add_message(role="user", content=user_input)
    
    # 轻量系统提示词(不含工具指南)
    lightweight_prompt = CORE_SYSTEM_PROMPT + "\n\n" + COMPANION_PROMPT_MODULE
    self.session_manager.update_system_prompt(lightweight_prompt)
    
    # 选择模型(无需 function calling)
    model_cfg = self.model_selector.select_for_task(
        needs_function_calling=False,
        model_key=self.model_key,
    )
    
    messages = self.session_manager.get_messages(
        max_tokens=model_cfg.context_window,
    )
    
    logger.info(
        "[CFTA] 快速聊天模式: model=%s, messages=%d, tools=None",
        model_cfg.key, len(messages),
    )
    
    # 发布模型调用事件
    await self.event_bus.emit(EventType.MODEL_CALL, ModelCallEvent(
        model_key=model_cfg.key,
        session_id=session_id,
    ))
    
    # 流式调用模型(不传 tools)
    stream = self.model_registry.chat_stream(
        model_key=model_cfg.key,
        messages=messages,
        tools=None,  # 关键: 不传工具,减少 prompt token
    )
    
    collected_content = ""
    last_usage = None
    
    async for chunk in _stream_with_timeout(stream, chunk_timeout=300):
        choice = chunk.choices[0] if chunk.choices else None
        if choice is None:
            continue
        
        delta_content = getattr(choice.delta, "content", None) or ""
        if delta_content:
            collected_content += delta_content
            yield delta_content
        
        last_usage = getattr(chunk, "usage", None)
    
    # 写入 session 历史
    self.session_manager.add_assistant_message(collected_content)
    
    # 发布响应完成事件
    await self.event_bus.emit(EventType.AGENT_RESPONSE, AgentResponseEvent(
        content=collected_content,
        total_steps=1,
        total_tokens=getattr(last_usage, "total_tokens", 0) if last_usage else 0,
        tool_calls_count=0,
        session_id=session_id,
    ))

快速回复生成

def _generate_fast_reply(self, tool_calls: list, fast_reply: str = None) -> str:
    """生成快速回复文本。
    
    Args:
        tool_calls: 检测到的工具调用列表
        fast_reply: 已有的快速回复(可选)
        
    Returns:
        快速回复文本
    """
    if fast_reply:
        return fast_reply
    
    if not tool_calls:
        return ""
    
    # 根据工具生成自然语言回复
    tool_names = []
    for tc in tool_calls:
        tool_name = tc.function.name
        
        # 工具名到中文映射
        name_mapping = {
            "search": "搜索信息",
            "weather": "查询天气",
            "calculator": "计算",
            "reminder": "设置提醒",
            "schedule": "查看日程",
            "music": "播放音乐",
        }
        
        for eng, chn in name_mapping.items():
            if eng in tool_name.lower():
                tool_names.append(chn)
                break
        else:
            tool_names.append(tool_name)
    
    if len(tool_names) == 1:
        return f"好的,我来帮你{tool_names[0]},稍等一下~"
    else:
        return f"好的,我来帮你{tool_names[0]},还有其他{len(tool_names)-1}个任务,让我一个个处理~"

🔄 核心模块三:process_deferred 异步工具处理

后台任务管理

async def process_deferred_tools(
    self,
    user_input: str,
    fast_reply: str,
    session_id: str,
) -> str | None:
    """CFTA Phase 2: 后台异步工具处理。
    
    当 chat_stream 检测到 tool_calls 时,调用此方法在后台执行工具。
    执行结果通过事件推送到 UI,不影响用户继续交互。
    
    Args:
        user_input: 原始用户输入
        fast_reply: 已发送的快速回复
        session_id: 会话 ID
        
    Returns:
        执行结果摘要字符串
    """
    logger.info(
        "[CFTA-deferred] 开始后台处理工具,用户输入:%s",
        user_input[:50],
    )
    
    # 获取当前 session
    session = self.session_manager.get_session(session_id)
    if not session:
        logger.warning("[CFTA-deferred] 会话不存在:%s", session_id)
        return None
    
    # 获取最后一条 assistant 消息中的 tool_calls
    messages = session.messages
    last_assistant = None
    for msg in reversed(messages):
        if msg.get("role") == "assistant" and msg.get("tool_calls"):
            last_assistant = msg
            break
    
    if not last_assistant:
        logger.warning("[CFTA-deferred] 未找到待执行的 tool_calls")
        return None
    
    tool_calls = last_assistant["tool_calls"]
    
    # --- 执行工具 ---
    executed_tools: list[str] = []
    result_parts: list[str] = []
    
    for tc in tool_calls:
        func_name = tc["function"]["name"]
        
        try:
            arguments = json.loads(tc["function"]["arguments"])
        except json.JSONDecodeError:
            arguments = {}
        
        resolved = self.tool_registry.resolve_function_name(func_name)
        tool_name = resolved[0] if resolved else func_name
        action_name = resolved[1] if resolved else ""
        
        # 发布工具调用事件
        await self.event_bus.emit(EventType.TOOL_CALL, ToolCallEvent(
            tool_name=tool_name,
            action_name=action_name,
            arguments=arguments,
            function_name=func_name,
            session_id=session_id,
        ))
        
        # 执行工具
        result = await self.tool_registry.call_function(func_name, arguments)
        
        # 发布工具结果事件
        await self.event_bus.emit(EventType.TOOL_RESULT, ToolResultEvent(
            tool_name=tool_name,
            action_name=action_name,
            status=result.status.value,
            output=result.output[:500] if result.output else "",
            error=result.error,
            duration_ms=result.duration_ms,
            session_id=session_id,
        ))
        
        logger.info(
            "[CFTA-deferred]   %s.%s → %s (%.0fms)",
            tool_name, action_name,
            result.status.value, result.duration_ms,
        )
        
        executed_tools.append(f"{tool_name}.{action_name}")
        
        # 添加工具结果到消息历史
        self.session_manager.add_tool_message(
            tool_call_id=tc["id"],
            content=result.output if result.is_success else f"错误:{result.error}",
        )
    
    # 构建结果摘要
    summary = f"已执行 {len(executed_tools)} 个工具:{', '.join(executed_tools)}"
    
    # 发布后台工具完成事件
    await self.event_bus.emit(EventType.DEFERRED_TOOL_RESULT, DeferredToolResultEvent(
        summary=summary,
        session_id=session_id,
    ))
    
    logger.info("[CFTA-deferred] 后台工具执行完成:%s", summary)
    
    return summary

UI 层任务调度

class AgentController:
    """Agent 控制器(UI 层)。"""
    
    def __init__(self, agent: Agent):
        self._agent = agent
        self._deferred_task: asyncio.Task = None  # 后台任务引用
    
    async def chat(self, message: str) -> None:
        """发送消息并处理响应。"""
        full_content = ""
        
        async for chunk in self._agent.chat_stream(message):
            full_content += chunk
            self.message_chunk.emit(chunk)
        
        # 检查是否有待处理的工具调用
        session = self._agent.session_manager.current_session
        has_pending_tools = self._check_pending_tools(session)
        
        if has_pending_tools:
            # 启动后台工具任务(Fire-and-Forget)
            self._start_deferred_task(message, full_content)
        else:
            self.message_finished.emit(full_content)
    
    def _start_deferred_task(self, message: str, fast_reply: str) -> None:
        """启动后台异步工具任务。"""
        # 取消之前的任务
        self._cancel_deferred_task()
        
        session_id = self._agent.session_manager.current_session.id
        
        # 创建后台任务
        self._deferred_task = asyncio.create_task(
            self._process_deferred_tools(message, fast_reply, session_id)
        )
        
        # 设置任务完成回调
        self._deferred_task.add_done_callback(
            lambda t: self._on_deferred_task_done(t, session_id)
        )
        
        logger.info("[CFTA] 已启动后台工具任务")
    
    def _cancel_deferred_task(self) -> None:
        """取消当前后台工具任务。"""
        if self._deferred_task and not self._deferred_task.done():
            self._deferred_task.cancel()
            logger.info("[CFTA] 已取消上一次后台工具任务")
        self._deferred_task = None
    
    async def _process_deferred_tools(
        self, message: str, fast_reply: str, session_id: str
    ) -> None:
        """后台异步工具处理(实际执行)。"""
        self.deferred_tool_started.emit()
        
        try:
            result = await self._agent.process_deferred_tools(
                message, fast_reply, session_id,
            )
            if result:
                self.deferred_tool_result.emit(result)
                logger.info("[CFTA] 异步工具完成,结果已推送到 UI")
        except asyncio.CancelledError:
            logger.info("[CFTA] 异步工具任务被取消")
        except Exception as e:
            logger.error("[CFTA] 异步工具处理失败: %s", e, exc_info=True)
    
    def _on_deferred_task_done(self, task: asyncio.Task, session_id: str) -> None:
        """后台任务完成回调。"""
        try:
            task.result()  # 检查是否有异常
        except asyncio.CancelledError:
            logger.info("[CFTA] 后台任务已取消")
        except Exception as e:
            logger.error("[CFTA] 后台任务异常: %s", e)
        finally:
            self._deferred_task = None

🔒 核心模块四:并发控制与任务取消

chat_lock 与 deferred_lock

class Agent:
    """AI Agent 主类。"""
    
    def __init__(self, ...):
        # 聊天序列化锁
        self._chat_lock: asyncio.Lock = None
        # CFTA: 异步工具执行锁
        self._deferred_lock: asyncio.Lock = None
    
    @property
    def chat_lock(self) -> asyncio.Lock:
        """懒加载聊天序列化锁。"""
        if self._chat_lock is None:
            self._chat_lock = asyncio.Lock()
        return self._chat_lock
    
    @property
    def deferred_lock(self) -> asyncio.Lock:
        """CFTA: 懒加载异步工具执行锁。"""
        if self._deferred_lock is None:
            self._deferred_lock = asyncio.Lock()
        return self._deferred_lock
    
    def cancel_deferred_tools(self) -> None:
        """取消当前后台工具任务。"""
        # 标记为取消状态
        self._deferred_cancelled = True

异步取消机制

class DeferredTaskManager:
    """后台任务管理器。"""
    
    def __init__(self):
        self._current_task: asyncio.Task = None
        self._cancelled = False
    
    async def run(self, coro) -> Any:
        """运行后台任务。"""
        self._cancelled = False
        
        # 创建任务
        self._current_task = asyncio.create_task(coro)
        
        try:
            # 等待任务完成
            result = await self._current_task
            return result
        except asyncio.CancelledError:
            logger.info("[Deferred] 任务被取消")
            raise
        finally:
            self._current_task = None
    
    def cancel(self) -> None:
        """取消当前任务。"""
        self._cancelled = True
        if self._current_task and not self._current_task.done():
            self._current_task.cancel()
            logger.info("[Deferred] 已发送取消信号")

工具执行超时控制

async def call_function_with_timeout(
    self,
    func_name: str,
    arguments: dict,
    timeout: float = 60.0,
) -> ToolResult:
    """带超时的工具执行。
    
    Args:
        func_name: 函数名
        arguments: 参数
        timeout: 超时时间(秒)
        
    Returns:
        工具执行结果
    """
    try:
        # 创建超时任务
        result = await asyncio.wait_for(
            self.call_function(func_name, arguments),
            timeout=timeout,
        )
        return result
    except asyncio.TimeoutError:
        logger.warning(
            "[Tool] 工具执行超时:%s (%.1f秒)",
            func_name, timeout,
        )
        return ToolResult(
            status=ToolResultStatus.ERROR,
            error=f"执行超时({timeout}秒)",
            output="",
        )

🎯 核心模块五:流式 TTS 协调

句子分割与流式播报

def _extract_complete_sentences(buffer: str) -> tuple[str, str]:
    """提取完整的句子,返回(已播放部分, 剩余部分)。
    
    句子以句号、逗号、感叹号、分号、问号结尾。
    """
    import re
    
    # 中文句子边界检测
    # 匹配到句子结束符为止的最长文本
    pattern = r'^([^,。!?;]+[,。!?;])+'
    match = re.match(pattern, buffer)
    
    if match:
        complete = match.group()
        remaining = buffer[len(complete):]
        return complete, remaining
    
    # 如果没有完整句子,检查是否有较长的片段(超过80字符也播放)
    if len(buffer) > 80:
        return buffer, ""
    
    return "", buffer


class TTSScheduler:
    """TTS 调度器,协调流式文本与语音播报。"""
    
    def __init__(self, tts_speaker):
        self._tts = tts_speaker
        self._buffer = ""
        self._playing = False
    
    async def feed(self, text_chunk: str) -> None:
        """接收文本片段。"""
        self._buffer += text_chunk
        
        # 提取完整句子
        complete, self._buffer = _extract_complete_sentences(self._buffer)
        
        if complete and not self._playing:
            await self._speak(complete)
    
    async def _speak(self, text: str) -> None:
        """播放文本。"""
        self._playing = True
        try:
            await self._tts.speak(text)
        finally:
            self._playing = False

📊 测试验证

性能对比

指标传统模式CFTA 模式提升
首次响应时间15-20 秒1-3 秒85%
UI 阻塞完全阻塞无阻塞
用户可交互
工具执行可见性黑盒实时日志
TTS 延迟15 秒1-3 秒85%

工具执行时间分布

总耗时:15 秒(假设)
├── AI 思考 + 生成 tool_call: 2 秒 (13%)
├── 工具执行(网络请求): 10 秒 (67%)  ← 后台执行
└── AI 生成最终回复: 3 秒 (20%)

CFTA 优化后:
├── AI 思考 + 生成 tool_call: 2 秒
├── 【立即返回】快速回复
└── 后台:工具执行 10 秒
    └── 完成后:UI 日志记录(不阻塞)

场景测试

场景传统模式CFTA 模式用户体验
天气查询等待 15 秒2 秒响应⭐⭐⭐⭐⭐
音乐播放等待 15 秒1 秒响应⭐⭐⭐⭐⭐
搜索信息等待 15 秒2 秒响应⭐⭐⭐⭐⭐
复杂任务串行执行可并发⭐⭐⭐⭐

💡 经验教训

1. chat_lock 的必要性

教训:初期未加锁,多个并发请求导致会话混乱。

解决方案:使用 asyncio.Lock 序列化聊天

async def chat_stream(self, message: str):
    await self.chat_lock.acquire()
    try:
        # 执行聊天逻辑
        ...
    finally:
        self.chat_lock.release()

2. 未完成 tool_calls 清理

教训:工具执行失败时,消息历史中的 tool_calls 没有正确标记,导致后续对话出错。

解决方案:在每次快速聊天前检查并清理

async def _chat_stream_voice_fast_impl(self, user_input: str):
    # 清理未完成的 tool_calls
    cleaned = self.session_manager.cleanup_incomplete_tool_calls()
    if cleaned > 0:
        logger.warning("已补全 %d 条缺失的 tool 响应消息", cleaned)

3. 事件回调的异常隔离

教训:某个订阅者的异常导致整个事件链中断。

解决方案:每个回调单独 try-catch

for sub_id, callback, filter_func in subscribers:
    try:
        await callback(event_data)
    except Exception as e:
        logger.error(f"事件回调执行失败:{event_type}.{sub_id}: {e}")
        # 继续处理其他订阅者

4. 后台任务的取消传播

教训:UI 层取消后台任务后,Agent 层仍在执行。

解决方案:双重取消机制

# UI 层
self._deferred_task.cancel()
self._agent.cancel_deferred_tools()

# Agent 层
async def process_deferred_tools(self, ...):
    if self._deferred_cancelled:
        raise asyncio.CancelledError()

📊 架构总结

CFTA 完整数据流

用户输入 → chat_stream → AI 快速响应
                              ↓
                    检测 tool_calls
                              ↓
                    【立即返回】快速回复
                              ↓
                    启动后台任务
                              ↓
         ┌────────────────────┴────────────────────┐
         ↓                                          ↓
    工具执行 1                               工具执行 2
         ↓                                          ↓
    事件通知                                    事件通知
         ↓                                          ↓
         └──────────────┬───────────────────────────┘
                        ↓
                   汇总结果
                        ↓
                   UI 日志记录
                   (不重复显示)

关键技术点

层次技术作用
并发控制asyncio.Lock序列化聊天
事件总线Observer Pattern解耦通知
后台任务asyncio.TaskFire-and-Forget
任务取消CancelledError优雅退出
超时控制asyncio.wait_for防止无限等待
TTS 协调句子分割流式播报

字数统计: 约 6,500 字
阅读时间: 约 16 分钟
代码行数: 约 500 行


上一篇文章回顾: 《音乐播放器开发:QtMultimedia 音频引擎与播放列表管理》——深入剖析本地音乐播放器实现。

下一篇文章预告: 《远程桥接并发控制:asyncio.Lock 与 Session 隔离机制》——如何安全地管理远程连接。