AgentScope 2.0 框架技术解析
1. 概述
AgentScope 2.0 是阿里通义实验室开源的 Python Agent 框架(Python ≥ 3.11,Apache-2.0)。它依靠模型自身的推理和工具调用能力,而不是用死板的 prompt 和固定编排去约束模型。本文基于仓库 main 分支(版本 2.0.8)的源码整理。
项目分两层:
- SDK 层 (
src/agentscope/下除app/外的部分,约 6 万行):用来组装单个 Agent。 - Agent Service 层 (
src/agentscope/app/,约 4.8 万行):基于 FastAPI 的多租户后端,配有 Web UI,提供会话、持久化、调度、IM 渠道、Agent Team 等能力。
模块地图
| 模块 | 作用 |
|---|---|
agent/ |
Agent 类(ReAct 主循环,约 3900 行)、A2AAgent |
model/ + formatter/ + credential/ |
对接 OpenAI、Anthropic、Gemini、DashScope、DeepSeek、Moonshot、火山、xAI、Ollama |
message/、event/ |
Msg 与内容块;统一的流式事件 |
middleware/ |
洋葱式 hook:预算、模型路由、RAG、长期记忆、tracing、TTS |
tool/ |
ToolBase、Toolkit、内置 Bash/Read/Write/Edit/Grep/Glob、MCP 适配 |
permission/ |
allow/deny/ask 规则引擎,Bash 用 tree-sitter 解析 |
workspace/ |
8 种执行环境(本地与沙箱)及 MCP 网关 |
mcp/ |
MCPClient 与 stdio / HTTP 配置 |
state/ |
AgentState,可序列化的运行时状态 |
pipeline/、sop/、classifier/ |
确定性编排、SOP 引擎、分类器 |
rag/、embedding/、realtime/、tts/ |
检索、向量、实时语音、语音合成 |
app/ |
路由、服务、存储、channel、Hub、WorkspaceManager |
核心特色
- Agent 是可持久化的状态机 :每一步由只读的
_next_action决定,所有状态放在AgentState中,可在任意一步挂起、存盘,再在另一个进程恢复。 - 用中间件扩展,而不是继承子类:reply、reasoning、acting、权限、模型调用、压缩、system prompt 都有 hook。
- 统一的流式事件协议:前端、终端 Console、IM 渠道消费同一套事件。
- 自带编码 Agent 基础设施:内置文件与命令工具、权限引擎、上下文压缩、8 种沙箱。
- 自带生产级服务层:多租户、持久化、调度、渠道、团队、RAG、MCP/Skill 市场。
- 信任模型:核心抽象是"一个 Agent 加一组工具,自主循环",只在需要确定性时才用 Pipeline。
2. Agent 主循环:可挂起的状态机
Agent 的 ReAct 循环每一步都由当前状态决定下一步是 Reasoning、Acting 还是 Exit,因此需要人工介入时可以直接退出,之后凭事件从断点恢复。代码在 src/agentscope/agent/_agent.py。
调用主线
scss
reply_stream / reply (288 / 332)
└─ _reply (892) 中间件链包装
└─ _reply_impl (1027) 主循环
├─ _next_action (3492) 状态 → Reasoning / Acting / Exit(只读)
├─ _reasoning (1633) → _call_model (3271)
└─ _acting (2711)
├─ _check_permission (2332)
└─ _execute_tool_call (2423)
_next_action 只读状态、不产生副作用,副作用统一由 _reply_impl 执行。它的判断规则:最后一条消息里有尚未执行、状态为 ALLOWED(或无等待中调用时为 PENDING)的工具调用 → Acting;存在等待确认或等待外部执行的调用 → Exit;否则继续 Reasoning。
HITL:挂起与恢复
- 工具需要用户确认时,Agent 发出
RequireUserConfirmEvent后直接退出,而不是在协程里阻塞等待。 - 恢复时把
UserConfirmResultEvent、ExternalExecutionResultEvent或UserInterruptEvent作为输入传给reply()。 - 挂起点没有单独字段:它就是
context最后一条消息中带状态的tool_call块。只要存下AgentState,就能在另一个进程继续。
中间件
MiddlewareBase(middleware/_base.py)提供 7 个 hook:on_reply、on_reasoning、on_acting、on_check_permission、on_model_call、on_compress_context、on_system_prompt。每个 hook 都是 next_handler 洋葱链。工具层另有 ToolMiddlewareBase.on_tool_call。
中间件实例每轮重建,跨轮次的数据必须存入 agent.state.middle_context,不能放在 self 上。
事件流
reply_stream() 输出细粒度事件:ReplyStart/End、ModelCallStart/End、text / thinking / data 块的 Start-Delta-End、ToolCall*、ToolResult*、RequireUserConfirm、RequireExternalExecution、ExceedMaxIters、HintBlock 等(event/_event.py)。前端、Console、IM 渠道、AG-UI 协议都消费这同一套事件。
3. 多 Agent
框架没有统一的多 Agent 编排器:默认让 LLM 用工具自己协调其他 Agent,只有需要确定性流程时才用代码写死的 Pipeline。三种模式共用一套契约:都实现同样签名的 reply_stream(),都支持挂起和恢复。
| 模式 | 位置 | 由谁决定协作 | 拓扑 | 适用场景 |
|---|---|---|---|---|
| Agent Team | app/_tool/、app/middleware/(依赖服务层) |
LLM(leader 调用工具) | 星型,异步消息加唤醒 | 开放式、可并行拆分的复杂任务 |
| Pipeline | pipeline/ |
开发者写的代码 | 固定流程 | 需要质量闭环或确定性流程 |
| A2A | agent/_a2a_agent.py |
外部系统 | 点对点远程调用 | 跨服务、跨框架互通 |
Agent Team:leader--worker 星型拓扑
Leader 是一个普通 Agent,只是多挂了 5 个团队工具:
| 工具 | 作用 |
|---|---|
TeamCreate |
当前会话成为 leader,建立团队 |
AgentCreate |
创建 worker;prompt 作为它的第一条消息,创建后立即开始工作 |
AgentInvite |
把用户已有的某个 Agent 借进团队 |
TeamSay |
点对点发消息或广播;worker 完成后必须用它向 leader 汇报 |
TeamDelete |
解散团队,并清理其中创建的 worker |
通信机制:
- 每个成员是独立会话,成员之间不是函数调用。
TeamSay把消息作为HintBlock写入对方收件箱(MessageBus),并发出 wakeup。- 收件方空闲时,
_wakeup_dispatcher.py拉起它的 run;正在运行时,InboxMiddleware在下一次 reasoning 前把消息注入上下文。 - Leader 派完活就可以结束这一轮,worker 汇报时会把它重新唤醒。
可靠性保障:TeamMemberLoopMiddleware 在 worker 未调用 TeamSay 就想结束时催促最多 3 次,超过则按错误结束。_subagent_hitl.py 把 worker 的人工确认请求投射到 leader 这边。工具描述明确要求所有成员只向 leader 汇报,不要创建"整合者"角色。
Pipeline:GoalPipeline
PipelineProtocol 只要求实现和 Agent 相同签名的 reply_stream(),所以 Pipeline 在外部看来就是一个 Agent,可以嵌套。内置的 GoalPipeline 是 executor--verifier 循环:
- executor 完成任务,提交结构化的
_ExecutionReport。 - verifier 返回
pass、fail或impossible,并附上修改意见。 fail时把意见交给 executor 再做一轮,最多max_iters(默认 10)轮。- verifier 默认每轮重置上下文,保证独立评判;迭代计数存在实例上,HITL 恢复后不会重新计数。
A2A
A2AAgent 不继承 Agent,但暴露相同接口。它把 Google A2A 协议的远程 Agent 包装成本地 Agent,并把远端返回转换为 AgentScope 事件流,可以直接放进 Pipeline。示例在 examples/a2a。
4. Workspace 与 Backend
Workspace 负责"环境"(生命周期、目录布局、MCP、skills、system prompt 片段),Backend 负责"执行"(读写文件、执行命令)。两者一一对应,共 8 对。所有内置工具只通过 workspace.get_backend() 做 I/O,所以同一份工具代码能在任何环境里运行。
8 种实现
| Workspace / Backend | 运行位置 | 通信方式 | 安装依赖 |
|---|---|---|---|
LocalWorkspace / LocalBackend |
本机目录,没有隔离 | asyncio 子进程、aiofiles、os.* |
无 |
DockerWorkspace / DockerBackend |
本机 Docker 容器 | aiodocker exec / archive | [workspace-docker] |
AppleContainerWorkspace |
macOS 原生容器 | container CLI |
本机安装 CLI |
BubblewrapWorkspace |
Linux 命名空间沙箱 | bwrap |
本机安装 bwrap |
K8sWorkspace |
Kubernetes Pod | kubernetes-asyncio exec | [workspace-k8s] |
E2BWorkspace |
E2B 云沙箱 | E2B SDK | [workspace-e2b] |
DaytonaWorkspace |
Daytona 云沙箱 | Daytona SDK | [workspace-daytona] |
OpenSandboxWorkspace |
OpenSandbox | OpenSandbox SDK | [workspace-opensandbox] |
除 Local 外,其余 7 种都继承 SandboxedWorkspaceBase。
Backend 只需实现 3 个原语
BackendBase(tool/_builtin/_backend.py)只有 3 个抽象方法:exec_shell(argv)、read_file(path)、write_file(path, data)。file_exists、is_dir、list_dir、stat、delete_path 等在基类里用 exec_shell 实现;LocalBackend 用 os.* 重写以提速。路径处理也放在 Backend 上,因为 Windows 宿主机操作 Linux 容器时分隔符不同。
生命周期
WorkspaceBase 规定:构造函数只存配置、不做 I/O;async with 进入时调用 initialize(),退出时调用 close();is_alive 保证 initialize() 幂等。
| 阶段 | LocalWorkspace | SandboxedWorkspaceBase(以 Docker 为例) |
|---|---|---|
| 构造 | 记录 workdir 绝对路径;直接创建 LocalBackend |
只存配置,_backend 为 None |
initialize() |
makedirs(workdir) → 从 .mcp 恢复 MCP 声明 → 建 skills/ 并按内容哈希复制 skill_paths |
_provision_backend()(按内容哈希复用或构建镜像,起 sleep infinity 容器)→ 恢复 .mcp → mkdir -p 目录 → 启动 MCP 网关(轮询 /health 最多 30s)→ 放入 skills |
close() |
只关闭有状态的 MCP 连接,不删除文件 | 关闭网关客户端 → _teardown_backend()(Linux 上先 chown,再 kill 并删除容器);finally 复位状态 |
reset() |
删除 .mcp、skills/、sessions/、data/ |
保留沙箱和网关,只清数据 |
workdir 布局:.mcp(MCP 声明)、skills/(initialize 时创建)、sessions/ 与 data/(按需创建)。没有挂载宿主机目录的 Docker 工作区是用完即焚的,close() 会连容器带文件一起删除。
WorkspaceManager(服务层)
- 隔离粒度 :
PER_SESSION、PER_AGENT(默认)、PER_USER。 - 缓存与回收 :
get_workspace()返回已初始化的实例并缓存;后台清理任务默认每 5 分钟检查一次,关闭闲置超过ttl(默认 3600s)的 Workspace。 - 预热 :缓冲区里放
asyncio.Future,请求来时已建好的直接拿走、正在建的等它建完;预热实例只发放一次、不回收复用,避免跨用户残留数据。 - Manager 自身也是异步上下文管理器,在 FastAPI lifespan 里启动和关闭。
5. MCP:配置、连接与调用
用 LocalWorkspace 时,MCP 在 Agent 进程内直连;用沙箱时,MCP 进程跑在沙箱内部,由网关转发。两条路径对上层暴露的接口完全一样。
MCPClient:一条配置即一个连接
MCPClient(mcp/_mcp_client.py)是 Pydantic 模型,字段包括 name、is_stateful、mcp_config(StdioMCPConfig 或 HttpMCPConfig)、enable_tools / disable_tools、execution_timeout。name 只能包含 [a-zA-Z0-9_-],因为会拼进工具名 mcp__{name}__{tool}。
| 有状态 | 无状态 | |
|---|---|---|
| stdio | 必须是这种,子进程一直运行 | 构造时报错 |
| HTTP | 保持长会话,适合登录态、cookie | 每次调用临时建会话 |
HTTP 的 URL 路径以 /sse 结尾时走 SSE,否则走 streamable HTTP。connect() 每次重建传输对象,失败或被取消时用 asyncio.shield 保护清理,避免 stdio 子进程成为孤儿。
每个工具包装成 MCPTool:工具名里的非法字符替换成 x;完整保留 inputSchema;服务器标注 readOnlyHint 的工具自动放行,其余一律需要用户确认;不向 MCP 工具注入 Agent 状态。
多配置管理(Workspace 层)
Workspace 用三张表管理 MCP,键都是 (agent_id, session_id):
| 表 | 内容 | 是否持久化 |
|---|---|---|
_mcp_specs |
声明挂了哪些 MCP | 是,写入 workdir/.mcp |
_mcp_instances |
当前活着的连接 | 否,按需创建 |
_mcp_last_used |
最近使用时间 | 否,用于 LRU |
规则:
- 读取 :
_declared_specs先查会话自己的配置;键不存在就复制一份default_mcps。值为[]表示明确不要任何 MCP。 - 修改 :
add_mcp/remove_mcp第一次修改时把"默认配置 + 改动"整份写入,此后该会话与default_mcps脱钩;未修改的会话不写盘。同一会话内 name 不能重复。 - 连接 :
list_mcps只为当前会话连接尚未连上的 MCP,每个会话一份独立实例;有状态连接超过max_live_stateful_mcps(默认至少 40)时按 LRU 关掉其他会话的连接,配置保留,下次自动重建。单个 MCP 失败只跳过它。 - 持久化 :
.mcp格式为{"version": 2, "mcps": {agent_id: {session_id: [MCPClient...]}}};文件损坏时回退到默认配置,坏条目只跳过一条;v1 格式自动升级。 - 运行时请求头 :
set_runtime_headers()仅支持 streamable HTTP,不进入model_dump,也不写入.mcp,适合刷新 token。
用户 MCP 库(服务层)
| 用户 MCP 库 | Workspace 的 .mcp |
|
|---|---|---|
| 存储 | 数据库 mcps 表(MCPRecord) |
workdir 下的文件 |
| 含义 | 期望状态:用户装了哪些 | 实际状态:某会话挂了哪些 |
| 接口 | /mcp |
/workspace/mcp |
两层通过 name 关联,所以 name 在同一用户下必须唯一。安装流程:
- 在 Hub(GitHub MCP Registry / ClawHub)找到一张
MCPCard:含${API_KEY}占位符的配置模板加一份inputs_schema。 - 用户填表 →
render_mcp()校验、替换占位符 → 存成MCPRecord写入用户库。 - 在会话里添加 →
POST /workspace/mcp/from-library:前端只传mcp_id,含密钥的配置不出服务端;逐个workspace.add_mcp(),失败不影响其他。 - 修改密钥 →
PATCH /mcp/{id},用保存的values基于原卡片重新渲染。
调用链
分三个阶段:① 创建 Toolkit 时 workspace.list_mcps() 连接(沙箱里是 POST /mcps 注册到网关);② 每轮推理前 Toolkit 调用 client.list_tools() 拉取工具;③ 模型返回工具调用后执行。
embed: node/85ef213a-e4a6
两条路径对 Agent 暴露相同的工具接口;沙箱多出的只是 exec_shell → shim → 网关这段转发,网关内部执行的仍是同一套 MCPTool 代码。
scss
Agent._execute_tool_call
├─ check_tool_available → _check_permission(非只读 MCP 工具 → ASK → 挂起等确认)
└─ _acting → Toolkit.call_tool(按 schema 修复参数)→ tool(**kwargs)
├─ 本地:MCPTool.call → session.call_tool → MCP 服务器
└─ 沙箱:GatewayMCPTool.__call__ → GatewayClient.exec_request
→ write_file(请求体) → exec_shell(python3 -c SHIM_SCRIPT ...)
→ [沙箱内] shim → 网关 POST /mcps/{mcp}/tools/{tool}
→ 沙箱内的 MCPTool → MCP 服务器
→ shim 向 stdout 打印 {status, body}
网关 (workspace/_mcp_gateway/_mcp_gateway_app.py)是沙箱内的 FastAPI 小应用,只监听回环地址,启动时不注册任何 MCP,每个请求带 agent_id 和 session_id。
shim (workspace/_gateway_shim.py)是内嵌在代码里的约 40 行 Python 脚本,把"HTTP 请求"转换为"执行命令 + 读 stdout":只用标准库 urllib(不假设有 curl);请求体经临时文件传递;响应 base64 编码,超过 4MB 写文件;退出码永远为 0,错误写在 JSON 里。代价是每次调用多起一个 Python 进程,换来的是 7 种沙箱只要能执行命令就能用 MCP,无需端口映射。
6. stdio MCP 协议原理
stdio MCP 服务器是一个由客户端启动的长期运行子进程:command + args 只在启动时用一次,之后所有请求都是通过 stdin/stdout 收发的 JSON-RPC 2.0 消息,每行一条。它不监听端口,只服务启动它的那一个客户端。
两种"参数"
| 启动参数 | 工具参数 | |
|---|---|---|
| 形式 | command、args、env、cwd |
JSON-RPC 请求里的 params.arguments |
| 何时使用 | connect() 启动子进程时,一次 |
每次 tools/call |
| 含义 | 服务器自己的配置,如允许访问的目录、API Key | 某个工具本次的入参 |
command 必须指向实现了 MCP 协议的程序(如 npx -y @modelcontextprotocol/server-filesystem /tmp、uvx mcp-server-fetch、python my_server.py、docker run -i --rm xxx/mcp)。grep 这类普通命令不认识 JSON-RPC,握手阶段就会失败。一个服务器进程对外提供多个工具,调用时靠 params.name 区分。
一次完整会话
以下是实测输出(两次 add 都在同一个 pid 里执行):
c
stdin → {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18",...}}
stdout ← {"jsonrpc":"2.0","id":1,"result":{"capabilities":{...},"serverInfo":{...}}}
stdin → {"jsonrpc":"2.0","method":"notifications/initialized"}
stdin → {"jsonrpc":"2.0","id":2,"method":"tools/list"}
stdout ← {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"add","inputSchema":{...}}]}}
stdin → {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}
stdout ← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"5"}],"isError":false}}
协议要点
- 分帧 :每条消息占一行,
\n分隔,UTF-8;stdout 只能写协议消息,日志必须写 stderr(自己写服务器最常踩的坑)。 - 三种消息 :请求(有
id和method)、响应(有id,带result或error)、通知(有method,无id,不回复)。 - 两类错误 :协议级错误放在
error;工具执行失败放在result并设isError: true,AgentScope 将其转为ToolChunk(state=ERROR)交给模型。 - 双向通信 :服务器也能主动发消息,如
notifications/progress、notifications/tools/list_changed、sampling/createMessage(请客户端调用大模型)、elicitation/create、roots/list。 - 为什么必须有状态:许多服务器要在多次调用之间保持状态(浏览器页面、数据库连接),每次启动新进程会丢失这些状态。
方法级调用链
工具参数从 Agent 到服务器函数的完整路径(mcp SDK 路径相对于 site-packages):
Toolkit.call_tool()(agentscope/tool/_toolkit.py:225):模型生成的参数字符串按 schema 修复成 dict。MCPTool.call()(agentscope/tool/_adapters.py:317)→session.call_tool(name, arguments=kwargs)。ClientSession.call_tool()(mcp/client/session.py:386):包成CallToolRequest。BaseSession.send_request()(mcp/shared/session.py:240):分配自增request_id,登记响应流,放入发送队列后挂起等待。stdin_writer()(mcp/client/stdio/__init__.py:166):model_dump_json()+\n写入子进程 stdin。- 服务端
stdin_reader()(mcp/server/stdio.py:60)逐行解析 → 按 method 分发到call_tool处理函数。 FastMCP.call_tool()(mcp/server/fastmcp/server.py:359)→Tool.run()→call_fn_with_arg_validation()(func_metadata.py:94):Pydantic 按函数签名校验,执行fn(**parsed)。- 结果经
stdout_writer()写回 stdout。 - 客户端
stdout_reader()读取 →_receive_loop()按id找到响应流,唤醒第 4 步。
发送和接收是两个独立的后台任务,靠 id 配对,所以同一管道上可以并发多个请求。实际开发中两端的解析都交给 SDK:服务器作者只写 @mcp.tool() 函数,mcp.run() 默认走 stdio,改为 mcp.run("streamable-http") 即可变成 HTTP 服务。
7. 状态与持久化
服务层每处理一轮对话都新建一个 Python Agent 对象,用完即弃;跨轮次延续的是数据库里的静态配置和会话状态。
agent_id 与 session_id
SDK 的 Agent 类没有 id 属性,只有 name。这两个 ID 是服务层数据库记录的主键,始终稳定。
| 是什么 | 何时生成 | 生命周期 | |
|---|---|---|---|
| agent_id | AgentRecord.data.id,智能体定义 |
用户创建智能体时 | 直到删除该智能体 |
| session_id | SessionRecord.id,一次对话 |
用户新建对话时 | 整个对话,跨轮次、跨重启 |
Workspace 不在创建时绑定会话,而是每次调用由调用方传入 ID,因为一个 Workspace 要服务多个会话:PER_AGENT 下同一智能体的所有会话共用一个 Workspace;Team 模式下 worker 共用 leader 的 Workspace。
三张表
| 表 | 记录 | 内容 | 写入时机 |
|---|---|---|---|
agents |
AgentRecord |
静态配置:名字、system prompt、context_config、react_config |
创建或编辑智能体时 |
sessions |
SessionRecord |
config(模型、workspace_id、会话名)+ state: AgentState |
创建时写 config;每轮结束更新 state |
messages |
Msg |
完整消息历史,主键 (session_id, msg_id) |
每轮结束 |
每张表把整条记录以 JSON 存入 payload,只把需要查询的字段单独建列和索引(_JsonRecordMixin),所以模型加字段一般无需迁移。messages 与 state.context 是两份数据:后者会被压缩和裁剪。
AgentState 字段
| 字段 | 内容 |
|---|---|
session_id |
会话 ID |
context |
未压缩的对话上下文,含工具调用和结果;挂起点也在这里 |
summary |
上下文压缩后的摘要 |
reply_context |
reply_id、迭代次数、结构化输出的 schema 和结果 |
permission_context |
权限模式、用户已同意的规则 |
tool_context |
文件读取缓存、已激活的工具组 |
tasks_context |
任务和计划列表 |
middle_context |
留给中间件跨轮次存数据 |
不持久化的内容
Agent对象及其 toolkit、model、中间件列表:每轮根据配置重建。- 中间件实例上的成员变量:丢失。
- MCP 活连接与 stdio 子进程:只在内存,Workspace 被回收后按
.mcp重建。 - 流式事件:一轮结束后被
log_trim裁剪。 - 未挂载宿主机目录的沙箱文件:TTL 到期后随容器删除。
保存时机
一轮运行的过程:获取会话锁 → 读 AgentRecord 与 SessionRecord.state → 新建 Agent → reply() → 保存 state 和消息 → 释放锁 → 丢弃 Agent。保存在 _chat.py 的 finally 里执行:
- 正常结束、出错、用户中断都会保存。
- 用
shield保护,HTTP 请求断开也会写完。 - 先保存再释放会话锁,防止其他进程读到旧状态。
HITL 挂起也属于一轮结束;用户确认后启动新的一轮,读出状态并传入 UserConfirmResultEvent 从断点继续。局限:一轮进行中不保存,进程被 kill -9 时这一轮的进展丢失,已产生的副作用不会回滚。
单独使用 SDK 时
SDK 没有自动持久化,需要自行读写 AgentState:
perl
from agentscope.state import AgentState
state = AgentState.model_validate_json(open("s.json").read()) # 或 AgentState(session_id="my-session")
agent = Agent(name="Friday", model=..., toolkit=..., state=state)
await agent.reply(msg)
open("s.json", "w").write(agent.state.model_dump_json())
AgentState.session_id 默认每次随机生成。如果把它传给 workspace.add_mcp(session_id=...) 却不保存 state,下次运行时就找不到之前加的 MCP。建议不传 ID(默认 ""),或固定一个 ID 并保存 state。
8. 环境搭建与学习路径
安装
仓库自带的 .venv 由 uv 创建,初始为空,直接运行示例会报 No module named 'agentscope'。在仓库根目录执行:
bash
uv pip install -e ".[dev]"
export DASHSCOPE_API_KEY=sk-xxx # examples/console 默认使用 DashScope
python examples/console/main.py --workdir ./ws
-e:可编辑安装,site-packages 里只放指向src/的指针,改源码立即生效。[dev]:full(全部功能依赖)加 pytest、pre-commit 及测试用的 fakeredis、aiosqlite、moto。- 引号是为了避免 zsh 把
[]当作通配符。 - uv 创建的虚拟环境没有 pip,装包要用
uv pip install。
依赖分组
| 分组 | 内容 |
|---|---|
| 核心(必装) | openai、anthropic、dashscope、mcp;docstring_parser、jsonschema、json_repair、tree_sitter、jinja2、python-frontmatter、pypdf;httpx、aiofiles;opentelemetry、rich |
model-* |
google-genai、ollama、xai-sdk |
service |
fastapi、uvicorn、apscheduler、ag-ui-protocol |
storage-* |
redis;sqlalchemy + alembic;aioboto3 |
channel |
lark-oapi、discord.py、dingtalk-stream |
workspace-* |
aiodocker、e2b、daytona、kubernetes-asyncio、opensandbox |
rag / vdb-* |
文档解析器;qdrant、milvus、mongodb、elasticsearch |
realtime / tui / a2a / memory-* |
websockets + sounddevice;textual;a2a-sdk;mem0ai、reme-ai |
核心依赖里 filetype、json5、python-datauri、python-socketio 在 src/ 中没有找到 import。
分阶段阅读路线
- 当用户用起来 (1--2 天):依次运行
examples/下的 console、tui、rag、long_term_memory、pipeline/goal、a2a;用reply_stream()打印事件类型。 - 核心主线 (3--5 天):按第 2 节的调用顺序读
_agent.py,对照message/、event/、state/;用tests/utils.py里的MockModel无需 API Key 即可单步调试;配合agent_basic_test.py、hitl_user_confirmation_test.py、agent_interrupt_test.py。 - 扩展点(3--5 天):各写一个中间件、自定义工具、权限规则,对比两个 formatter,调小压缩阈值观察触发。
- 多 Agent 与服务层 (约 1 周):先读
GoalPipeline(约 300 行),再运行examples/agent_service+web_ui,追TeamSay→ MessageBus →_wakeup_dispatcher→InboxMiddleware。 - 参与贡献 :从小范围的
fix(...)提交学习项目习惯,提交前运行pre-commit run --all-files和pytest tests。
常用调试技巧:Base.__subclasses__() 递归列出子类(只含已导入的类);Cls.__mro__ 查看方法解析顺序;VS Code 中 ⌘F12 跳转到实现。
阅读中发现的问题
- 沙箱路径的
GatewayMCPTool直接重写__call__且未调用super().__init__,宿主机侧的ToolMiddlewareBase对沙箱 MCP 工具不生效。 MCPTool会把工具名中的非法字符替换成x,而GatewayMCPTool直接拼接tool.name;工具名含点号时,沙箱里生成的工具名可能被模型厂商拒绝。尚未用测试验证。