copaw梳理

一、整体架构

CoPaw 是一个支持多渠道通信的个人 AI 助手系统,采用分层架构设计:
架构层次:

  1. 用户层 - Console Web UI、钉钉、飞书、QQ、Discord、iMessage 等多渠道
  2. 应用层 - FastAPI 应用,包含动态路由和多 Agent 管理
  3. 核心 Agent 层 - CoPawAgent(内置工具、技能、记忆管理、安全拦截)
  4. 支撑服务层 - 模型提供商、技能管理、安全模块、Token 追踪等
  5. 基础设施层 - AgentScope Runtime、FastAPI、Redis、文件系统

二、核心模块

1. agents/ - Agent 核心实现
  • react_agent.py - CoPawAgent 主类,继承 ReActAgent + ToolGuardMixin
  • skills_manager.py - 技能管理器,动态加载技能
  • tools/ - 内置工具(shell、文件读写、浏览器控制等)
  • skills/ - 内置技能(定时任务、文档处理、新闻摘要等)
  • memory/ - 记忆管理(向量搜索 + 全文搜索)
2. app/ - FastAPI 应用
  • _app.py - 应用入口,动态路由到不同的 Workspace
  • workspace.py - 独立工作空间,每个 Workspace 是完整的 Agent 实例
  • channels/ - 渠道管理(钉钉、飞书、Discord、QQ 等)
  • crons/ - 定时任务管理
  • routers/ - API 路由(agent、config、providers、skills)
3. providers/ - 模型提供商管理
  • 支持多种提供商:ModelScope、OpenAI、Anthropic、Ollama 、自定义等
  • provider_manager.py - 统一管理不同提供商
4. security/ - 安全模块
  • tool_guard/ - 工具守卫规则引擎,拦截危险操作
  • skill_scanner/ - 技能扫描器,安全检查技能代码
5. console/ - 前端应用
  • React 18 + TypeScript + Vite
  • 主要页面:聊天、Agent 配置、控制面板、设置
6. cli/-命令行
  • main.py/ - 定义命令行指令功能,定义了chat、cron、skills、model等多个指令
  • 指令均使用click包装
    用户要求我用中文讲解这个文件的功能和逻辑。这是一个 FastAPI 应用的核心文件,让我分析一下它的主要功能和逻辑。
    让我先阅读这个文件,然后给出详细的讲解。我来详细讲解这个文件的功能和逻辑。

三、后端服务管理

_app.py 是 CoPaw 项目的 FastAPI 应用核心文件,负责初始化和配置整个 Web 服务器,包括多 Agent 管理、路由配置、静态文件服务等。

1. DynamicMultiAgentRunner 类

这是核心的多 Agent 路由机制:

  • 作用: 动态将请求路由到正确的 Agent 工作区运行器
  • 原理: 通过检查请求中的 X-Agent-Id header 来确定使用哪个 Agent
  • 关键方法:
    • _get_workspace_runner() - 根据 agent_id 获取对应的工作区运行器
    • stream_query() - 路由流式查询请求
  • query_handler() - 路由查询处理请求
    这个设计允许单个 FastAPI 应用同时服务多个 Agent 实例。

2. AgentApp 初始化

agent_app = AgentApp( app_name="Friday", app_description="A helpful assistant", runner=runner, )
创建 AgentScope 运行时应用实例,使用我们自定义的 DynamicMultiAgentRunner 作为运行器。

3. Lifespan 生命周期管理

应用启动和关闭时的关键操作:
启动阶段:

  • 设置日志文件处理器
  • 迁移和初始化: 检查旧配置并迁移到默认 Agent
  • 多 Agent 管理器初始化: 创建 MultiAgentManager 并启动所有配置的 Agent
  • Provider 管理: 初始化模型提供者管理器(单例)
  • 连接组件: 将 DynamicMultiAgentRunner 连接到 MultiAgentManager
  • 审批服务: 使用默认 Agent 的 channel_manager 设置审批服务
    关闭阶段:
  • 停止所有 Agent 及其组件
  • 清理资源

4. FastAPI 应用配置

app = FastAPI( lifespan=lifespan, docs_url="/docs" if DOCS_ENABLED else None, ... )
创建 FastAPI 应用实例,配置生命周期管理和文档路由。

5. 中间件配置

AgentContextMiddleware:
app.add_middleware(AgentContextMiddleware)
为 Agent 范围的路由添加上下文中间件,用于提取和管理 Agent ID。
CORS 中间件:
if CORS_ORIGINS: app.add_middleware(CORSMiddleware, ...)
根据配置添加跨域资源共享支持。

6. 路由配置

主要路由:

  1. 根路由: GET / - 服务 Web Console 的 index.html
  2. API 路由: /api/* - 通用 API 端点
  3. Agent 范围路由: /api/agents/{agentId}/* - 特定 Agent 的聊天、控制等
  4. Agent 应用路由: /api/agent/* - AgentScope 的查询接口
  5. 语音路由: /voice/* - Twilio 集成的语音端点(POST、WebSocket)
  6. 版本路由: /api/version - 返回 CoPaw 版本

核心设计思想

  1. 多 Agent 架构: 通过 DynamicMultiAgentRunner 实现单个应用服务多个 Agent
  2. 动态路由: 基于 HTTP header 动态选择 Agent,无需为每个 Agent 创建独立实例
  3. 生命周期管理: 统一管理所有 Agent 的启动、运行和关闭
  4. 渐进式迁移: 支持从旧版本配置平滑迁移到新的多 Agent 架构
  5. 灵活性: 通过环境变量和配置文件控制各种功能(CORS、文档、静态文件路径等)

数据流
客户端请求 → AgentContextMiddleware(提取 Agent ID) → DynamicMultiAgentRunner(路由到对应 Agent) → 具体的工作区运行器处理请求 → 返回响应
这个文件是整个 CoPaw 服务的入口点,协调了所有核心组件的初始化和交互。

记忆模块
模型使用的库是阿里开源的reme,在copaw中做了包装使用

python 复制代码
class Workspace:
    """Single agent workspace with complete runtime components.

    Each Workspace is an independent agent instance with its own:
    - Runner: Processes agent requests
    - ChannelManager: Manages communication channels
    - MemoryManager: Manages conversation memory
    - MCPClientManager: Manages MCP tool clients
    - CronManager: Manages scheduled tasks

    All components use existing single-agent code without modification.
    """

    def __init__(self, agent_id: str, workspace_dir: str):
        """Initialize agent instance.

        Args:
            agent_id: Unique agent identifier
            workspace_dir: Path to agent's workspace directory
        """
        self.agent_id = agent_id
        self.workspace_dir = Path(workspace_dir).expanduser()
        self.workspace_dir.mkdir(parents=True, exist_ok=True)

        # All components are None until start() is called (lazy loading)
        self._runner: Optional[AgentRunner] = None
        self._channel_manager: Optional["BaseChannel"] = None
        # MemoryManager集成至ReMeLight
        self._memory_manager: Optional[MemoryManager] = None
        self._mcp_manager: Optional[MCPClientManager] = None
        self._cron_manager: Optional["CronManager"] = None
        self._chat_manager = None
        self._config = None
        self._config_watcher = None
        self._mcp_config_watcher = None
        self._started = False

ReMe 是一个专为 AI 智能体 打造的记忆管理框架,同时提供基于文件系统 和基于向量库的记忆系统。看copaw的代码使用了文件系统做存储,注册了文件读、写、编辑工具。

python 复制代码
class MemoryManager(ReMeLight):
        self.summary_toolkit = Toolkit()
        self.summary_toolkit.register_tool_function(read_file)
        self.summary_toolkit.register_tool_function(write_file)
        self.summary_toolkit.register_tool_function(edit_file)

AgentRunner集成至agentscope_runtime的Runner,让 Agent 动起来、干活、执行思考循环的核心组件, 执行 Agent 的主循环(think → act → observe)。

python 复制代码
class AgentRunner(Runner):
    def __init__(
        self,
        agent_id: str = "default",
        workspace_dir: Path | None = None,
    ) -> None:
        super().__init__()
        self.framework_type = "agentscope"
        self.agent_id = agent_id  # Store agent_id for config loading
        self.workspace_dir = (
            workspace_dir  # Store workspace_dir for prompt building
        )
        self._chat_manager = None  # Store chat_manager reference
        self._mcp_manager = None  # MCP client manager for hot-reload
        self.memory_manager: MemoryManager | None = None

    async def query_handler() # 处理输入提示词语llm请求
        ......
        if not approval_consumed and query and _is_command(query):
            logger.info("Command path: %s", query.strip()[:50])
            # 判断是否为"/"指令
            async for msg, last in run_command_path(request, msgs, self):
                yield msg, last
            return
        ......
        # Get MCP clients from manager (hot-reloadable)
        mcp_clients = []
        if self._mcp_manager is not None:
             mcp_clients = await self._mcp_manager.get_clients()
        # 构建copawagent,继承ReActAgent功能,CoPaw Agent内根据运营商创建聊天实例,
        # 并整合integrated tools, skills, and memory management.
        # src/copaw/agents/react_agent.py 内copawagent类
        agent = CoPawAgent(
                env_context=env_context,
                mcp_clients=mcp_clients,
                memory_manager=self.memory_manager,
                request_context={
                    "session_id": session_id,
                    "user_id": user_id,
                    "channel": channel,
                    "agent_id": self.agent_id,
                },
                max_iters=max_iters,
                max_input_length=max_input_length,
                memory_compact_threshold=(
                    running_config.memory_compact_threshold
                ),
                memory_compact_reserve=running_config.memory_compact_reserve,
                enable_tool_result_compact=(
                    running_config.enable_tool_result_compact
                ),
                tool_result_compact_keep_n=(
                    running_config.tool_result_compact_keep_n
                ),
                language=language,
                workspace_dir=self.workspace_dir,
            )
            await agent.register_mcp_clients()

AgentRunner集成至agentscope_runtime的Runner,让 Agent 动起来、干活、执行思考循环的核心组件, 执行 Agent 的主循环(think → act → observe)。

python 复制代码
class AgentRunner(Runner):
    def __init__(
        self,
        agent_id: str = "default",
        workspace_dir: Path | None = None,
    ) -> None:
        super().__init__()
        self.framework_type = "agentscope"
        self.agent_id = agent_id  # Store agent_id for config loading
        self.workspace_dir = (
            workspace_dir  # Store workspace_dir for prompt building
        )
        self._chat_manager = None  # Store chat_manager reference
        self._mcp_manager = None  # MCP client manager for hot-reload
        self.memory_manager: MemoryManager | None = None

    async def query_handler() # 处理输入提示词语llm请求
        ......
        if not approval_consumed and query and _is_command(query):
            logger.info("Command path: %s", query.strip()[:50])
            # 判断是否为"/"指令
            async for msg, last in run_command_path(request, msgs, self):
                yield msg, last
            return
        ......
        # Get MCP clients from manager (hot-reloadable)
        mcp_clients = []
        if self._mcp_manager is not None:
             mcp_clients = await self._mcp_manager.get_clients()
        # 构建copawagent,继承ReActAgent功能,CoPaw Agent内根据运营商创建聊天实例,
        # 并整合integrated tools, skills, and memory management.
        # src/copaw/agents/react_agent.py 内copawagent类
        agent = CoPawAgent(
                env_context=env_context,
                mcp_clients=mcp_clients,
                memory_manager=self.memory_manager,
                request_context={
                    "session_id": session_id,
                    "user_id": user_id,
                    "channel": channel,
                    "agent_id": self.agent_id,
                },
                max_iters=max_iters,
                max_input_length=max_input_length,
                memory_compact_threshold=(
                    running_config.memory_compact_threshold
                ),
                memory_compact_reserve=running_config.memory_compact_reserve,
                enable_tool_result_compact=(
                    running_config.enable_tool_result_compact
                ),
                tool_result_compact_keep_n=(
                    running_config.tool_result_compact_keep_n
                ),
                language=language,
                workspace_dir=self.workspace_dir,
            )
            await agent.register_mcp_clients()


def list_available_skills(workspace_dir: Path) -> list[str]:
    """
    List all available skills in active_skills directory.
    Args:
        workspace_dir: Workspace directory path.
    Returns:
        List of skill names.
        skills列表获取是从存储的本地目录中获取,并确认SKILL.md存在
    """
    active_skills = get_active_skills_dir(workspace_dir)
    if not active_skills.exists():
        return []
    return [
        d.name
        for d in active_skills.iterdir()
        if d.is_dir() and (d / "SKILL.md").exists()
    ]
    

class Toolkit(StateModule):
        def register_agent_skill(......):
# 在注册skills时,把SKILL.md文件的name、description 、目录记录下来:
        with open(path_skill_md, "r", encoding="utf-8") as f:
            post = frontmatter.load(f)
        name = post.get("name", None)
        description = post.get("description", None)
        if not name or not description:
            raise ValueError(
                f"The SKILL.md file in '{skill_dir}' must have a YAML Front "
                "Matter including `name` and `description` fields.",
            )
        name, description = str(name), str(description)
        if name in self.skills:
            raise ValueError(
                f"An agent skill with name '{name}' is already registered "
                "in the toolkit.",
            )
        self.skills[name] = AgentSkill(
            name=name,
            description=description,
            dir=skill_dir,
        )

copaw agent 执行之后主要模型逻辑:

python 复制代码
class ReActAgent(ReActAgentBase):
  async def reply(
        self,
        msg: Msg | list[Msg] | None = None,
        structured_model: Type[BaseModel] | None = None,
    ) -> Msg:
    ......
    # 一个有限定次数的循环执行模型思考、工具调用、观察
    for _ in range(self.max_iters):
            # -------------- Memory compression --------------
            await self._compress_memory_if_needed()

            # -------------- The reasoning process --------------
            msg_reasoning = await self._reasoning(tool_choice)

            # -------------- The acting process --------------
            futures = [
                self._acting(tool_call)
                for tool_call in msg_reasoning.get_content_blocks(
                    "tool_use",
                )
            ]
            # Parallel tool calls or not
            if self.parallel_tool_calls:
                structured_outputs = await asyncio.gather(*futures)
            else:
                # Sequential tool calls
                structured_outputs = [await _ for _ in futures]

            # -------------- Check for exit condition --------------
            # If structured output is still not satisfied
            if self._required_structured_model:
                # Remove None results
                structured_outputs = [_ for _ in structured_outputs if _]

                msg_hint = None
                # If the acting step generates structured outputs
                if structured_outputs:
                    # Cache the structured output data
                    structured_output = structured_outputs[-1]

                    # Prepare textual response
                    if msg_reasoning.has_content_blocks("text"):
                        # Re-use the existing text response if any to avoid
                        # duplicate text generation
                        reply_msg = Msg(
                            self.name,
                            msg_reasoning.get_content_blocks("text"),
                            "assistant",
                            metadata=structured_output,
                        )
                        break

                    # Generate a textual response in the next iteration
                    msg_hint = Msg(
                        "user",
                        "<system-hint>Now generate a text "
                        "response based on your current situation"
                        "</system-hint>",
                        "user",
                    )
                    await self.memory.add(
                        msg_hint,
                        marks=_MemoryMark.HINT,
                    )

                    # Just generate text response in the next reasoning step
                    tool_choice = "none"
                    # The structured output is generated successfully
                    self._required_structured_model = None

                elif not msg_reasoning.has_content_blocks("tool_use"):
                    # If structured output is required but no tool call is
                    # made, remind the llm to go on the task
                    msg_hint = Msg(
                        "user",
                        "<system-hint>Structured output is "
                        f"required, go on to finish your task or call "
                        f"'{self.finish_function_name}' to generate the "
                        f"required structured output.</system-hint>",
                        "user",
                    )
                    await self.memory.add(msg_hint, marks=_MemoryMark.HINT)
                    # Require tool call in the next reasoning step
                    tool_choice = "required"

                if msg_hint and self.print_hint_msg:
                    await self.print(msg_hint)

            elif not msg_reasoning.has_content_blocks("tool_use"):
                # Exit the loop when no structured output is required (or
                # already satisfied) and only text response is generated
                msg_reasoning.metadata = structured_output
                reply_msg = msg_reasoning
                break
                
#模型思考
async def _reasoning(
        self,
        tool_choice: Literal["auto", "none", "required"] | None = None,
    ) -> Msg:
    ......
    # 模型附带工具调用
    res = await self.model(
            prompt,
            tools=self.toolkit.get_json_schemas(),
            tool_choice=tool_choice,
        )
     ......

# 根据模型返回处理,如果有工具则做调用
async def _acting(self, tool_call: ToolUseBlock) -> dict | None:
        tool_res_msg = Msg(
            "system",
            [
                ToolResultBlock(
                    type="tool_result",
                    id=tool_call["id"],
                    name=tool_call["name"],
                    output=[],
                ),
            ],
            "system",
        )
        try:
            # Execute the tool call
            tool_res = await self.toolkit.call_tool_function(tool_call)
            ......

channel频道管理支持的内建:

python 复制代码
_BUILTIN_SPECS: dict[str, tuple[str, str]] = {
    "imessage": (".imessage", "IMessageChannel"),
    "discord": (".discord_", "DiscordChannel"),
    "dingtalk": (".dingtalk", "DingTalkChannel"),
    "feishu": (".feishu", "FeishuChannel"),
    "qq": (".qq", "QQChannel"),
    "telegram": (".telegram", "TelegramChannel"),
    "mattermost": (".mattermost", "MattermostChannel"),
    "mqtt": (".mqtt", "MQTTChannel"),
    "console": (".console", "ConsoleChannel"),
    "matrix": (".matrix", "MatrixChannel"),
    "voice": (".voice", "VoiceChannel"),
    "wecom": (".wecom", "WecomChannel"),
    "xiaoyi": (".xiaoyi", "XiaoYiChannel"),
}
相关推荐
HIT_Weston11 小时前
45、【Agent】【OpenCode】本地代理分析(请求&接收回调)
人工智能·agent·opencode
逻辑君12 小时前
认知神经科学研究报告【20260010】
人工智能·深度学习·神经网络·机器学习
星河耀银海12 小时前
远控体验分享:安全与实用性参考
人工智能·安全·微服务
企业架构师老王12 小时前
2026企业架构演进:科普Agent(龙虾)如何从“极客玩具”走向实在Agent规模化落地?
人工智能·ai·架构
GreenTea12 小时前
一文搞懂Harness Engineering与Meta-Harness
前端·人工智能·后端
鬼先生_sir12 小时前
Spring AI Alibaba 1.1.2.2 完整知识点库
人工智能·ai·agent·源码解析·springai
深念Y13 小时前
豆包AI能力集成方案:基于会话管理的API网关设计
人工智能
龙文浩_13 小时前
Attention Mechanism: From Theory to Code
人工智能·深度学习·神经网络·学习·自然语言处理
ulimate_13 小时前
八卡算力、三个Baseline算法(WALLOSS、pi0、DreamZero)
人工智能
深小乐13 小时前
AI 周刊【2026.04.06-04.12】:Anthropic 藏起最强模型、AI 社会矛盾激化、"欢乐马"登顶
人工智能