跳到主要内容

数据截至 (上游 commit 3a4e2ae3eec0)

第 6 章 · 精华、边界与对比

这一章讲:读完能带走什么、它在哪会崩、以及和同类项目比它取舍在哪。


6.1 巧妙之处(可借鉴的技术)

① 决策器只读,执行器只写

妙在哪: 循环体里一句「该不该结束」的判断都没有,全在 _next_action 里。这让「加一种新的退出条件」变成往决策表里加一个分支,而不是往循环体里塞一个 break。

更深一层:因为决策器是纯函数,同一个状态永远推导出同一个动作。挂起后重新调 reply(),不需要任何断点标记,状态自己会把循环带回正确的位置。

依据:src/agentscope/agent/_agent.py:3248_next_action,docstring 明写 "Read-only: all side effects are performed by the caller"

② 用「可空的退出事件」表达「停下来 ≠ 结束了」

妙在哪: 一个 list | None 字段解决了 HITL 最难的表达问题。

class Exit(BaseModel):
exit_msg: Msg
exit_events: list[AgentEvent] | None = None # None = 挂起,不是结束

消费方(前端、服务层)的判据也随之极简:收到 ReplyEndEvent 才算这轮结束,没收到就留着会话等结果。

依据:src/agentscope/agent/_utils.py:39Exit,以及 src/agentscope/agent/_agent.py:1034-1039 的分支。

bypass_immune:把「危险」和「偏好」分开

妙在哪: 大多数权限系统只有 allow/deny/ask 三态,结果 allow 规则会把真正的危险操作一起消音。

AgentScope 给 ASK 加了一个布尔标记,区分「我倾向问一下」和「这事真危险」。于是同一条 allow 规则,在两种 ASK 面前行为不同;同一个安全 ASK,在 BYPASS 下被跳过、在 DONT_ASK 下变成 DENY。

并且它诚实地承认了这个标记的边界:调用方对两种 ASK 一视同仁,区别只在引擎内部。这种「明确划定抽象泄漏边界」的注释很少见。

依据:src/agentscope/permission/_decision.py:37-68PermissionDecision.bypass_immune 的长注释)。

④ 压缩切点用不动点迭代

妙在哪: 「切上下文时别拆散工具调用和它的结果」听起来是个一次性修正,实际不是——推移边界会制造新的孤儿

初始切点 [调用A 结果A 调用B] | [结果B] ← 结果B 是孤儿
推移一次 [调用A 结果A] | [调用B 结果B] ← 修好了?
再检查 ...但如果消息里还有交错的 C,又出孤儿

所以源码用 while True 迭代到稳定,而不是修一次就走。

依据:src/agentscope/agent/_agent.py:2754-2777

⑤ inbox 交接:两个临界区互斥

妙在哪: 「推消息 + 按需唤醒」这个看似简单的操作,朴素实现会丢消息。AgentScope 的解法不是加更复杂的状态机,而是让生产者和消费者的两个极小临界区共享一把锁——两种交错顺序都自然安全。

再加一条:唤醒只在「无消费者」时产生,所以永远不会唤醒一个无事可做的会话。

依据:src/agentscope/app/_bus_ops.py:141-205(注释本身就是一份并发推导)。

⑥ 错误信息就是给模型的指令

妙在哪: 工具没激活时,报错不是 KeyError,而是:

ToolGroupInactiveError: The tool 'X' in group 'Y' is currently inactive. You should first activate the group by calling the 'ResetTools' tool.

模型读到这句话就知道下一步该干什么,自己就恢复了。整个框架里,凡是模型能自己修复的错误都写成这种「诊断 + 处方」的格式并回喂给模型;只有开发者才能修的错误DeveloperOrientedException)才真的抛出去。

这条分界线贯穿全仓库,是它「production-ready」气质的主要来源。

依据:src/agentscope/tool/_toolkit.py:584-590;异常分类见 src/agentscope/exception/

番外:几个小而好的处理

处理在哪为什么好
base64 增量不能字符串拼接src/agentscope/tool/_response.py:13 _merge_base64_chunks每段各带 padding,直接拼会损坏字节流
助手产出的音频不进上下文src/agentscope/agent/_agent.py:3208-3223在写入口过滤一次,下游所有遍历者都不用记着这事
空内容的流式载体块被吞掉src/agentscope/model/_base.py:267-276吸收 usage 元数据但不污染可见流
短危险模式用词边界匹配src/agentscope/tool/_builtin/_bash_parser.py:668-673否则 git add 会匹上危险模式 dd
中断时 flush 队列再 uncancel()src/agentscope/agent/_agent.py:2105-2113让并发路径和串行路径的中断语义统一成「靠事件判断」

6.2 边界与局限(诚实版)

① token 估算是字节除以 4

count_tokenssrc/agentscope/model/_base.py:369)不接 tokenizer,多模态块按固定 2000 token 记。对中文会明显高估(一个汉字 3 字节记 0.75 token,实际常低于此)。结果是压缩比该触发的时候更早触发——保守,但如果你按 token 计费做预算控制,这个数不能当账单用。

子类可覆盖,仓库里没有默认覆盖实现。

ReActConfig.stop_on_reject 是死配置

字段定义在 src/agentscope/agent/_config.py:320,描述是「工具被拒时是否停止回复」。src/ 全量 grep,除定义处没有任何读取点(另有一处只是 examples/web_ui 前端的 TypeScript 类型声明)。配上它不会有任何效果。

③ 纯 SDK 用法没有多 agent 编排

第 5 章说过:1.x 的 msghub / pipeline 已被完全移除,协作能力(TeamSay / AgentCreate)都在 agentscope.app 下,依赖存储、消息总线、会话记录。

想在不起服务的情况下让两个 agent 对话,你得自己写胶水。 这是从 1.x 迁移时最大的落差。

④ 函数工具与 MCP 工具的权限粒度只到工具名

ToolBase.match_rule 默认实现只认 rule_content=Nonesrc/agentscope/tool/_base.py:305-314)。只有 Bash / Read / Write / Edit / Grep / Glob 重写了它以支持模式匹配。

所以 MCP 工具要么整个放行,要么每次都问,没有「只允许查询、不允许写入」这种中间态——除非你自己包一层重写 match_rule

BYPASS 模式会跳过所有安全检查

源码把话说得很重(src/agentscope/permission/_types.py:52-58):BYPASS 下 rm -rf /、写 ~/.bashrc、命令注入模式全部不拦,只剩用户配的 deny/ask 规则。

它明确建议:无人值守但仍在乎安全,用 DONT_ASK。这个建议值得当成使用纪律,而不是可选项。

⑥ 状态注入的工具还没防并发

_acting_impl 的注释里有一条自留 TODO(src/agentscope/agent/_agent.py:2592-2596):

Tools with is_state_injected=True receive the live agent.state object. Offloading such tools to a background task may cause concurrent state mutations. TODO: block background offloading for state-injected tools.

即:如果你写一个把工具转后台的 on_acting 中间件,而工具又注入了 agent 状态,可能出并发写。当前没有拦截。

⑦ 依赖不轻

基础依赖里就包含 anthropicopenaidashscopemcpnumpytree_sitteropentelemetry-* 全家桶(pyproject.toml:21-50)。只想用 OpenAI 也会装进 Anthropic SDK 和 tree-sitter。要求 Python ≥ 3.11(pyproject.toml:20)。


6.3 横向对比

和同 shelf 兄弟的取舍差异

关切AgentScope 2.0 的取舍常见的另一种取舍
循环怎么写纯函数决策器 + 执行器分离状态判断散在循环体里,或用图/节点编排
多 agent 协作模型调工具自己发消息(服务层)框架提供 pipeline / graph 编排(SDK 层)
人工确认一等公民:可挂起、可存盘、可恢复事后回调,或阻塞式 input()
权限独立引擎,五模式 × 三类规则 × 安全豁免标记工具级白名单,或干脆没有
上下文管理结构化摘要 + 卸载到工作区 + 状态注入滑动窗口截断,或向量检索
沙箱三方法抽象,八种后端只支持本地或只支持 Docker
部署形态自带 FastAPI 多租户服务 + Web UI只给库,部署自理

一句话定位

如果把 agent 框架分成三类:

研究/编排型 ──────── 生产/运行时型 ──────── 端到端产品型
(画图、节点、 (AgentScope 2.0) (现成的 IDE/助手)
可视化流程)

AgentScope 2.0 明确站中间:它不帮你画流程图,也不给你现成产品,它给你一个「能扛住线上流量的 agent 循环 + 一个能直接起服务的壳」。 它最像的参照物不是编排框架,而是 Claude Code 这类编码 agent 的开源骨架——权限模式、Bash 静态分析、<system-reminder> 提示注入、技能目录这些设计,都能看出同一条产品谱系的痕迹。

什么时候该选它

你的情况建议
要做能改文件/跑命令、且需要人工把关的 agent很合适,权限系统是现成的
要做多租户线上服务很合适,app 层直接可用
要研究多 agent 拓扑、辩论、投票不合适,2.0 没有这类原语了
只想快速接个模型跑个循环偏重,依赖和抽象层都多
从 AgentScope 1.x 迁移注意:这是重写,不是升级

6.4 学习路径建议

想真正吃透,按这个顺序读源码:

① agent/_utils.py 30 行,三个动作类型 —— 先建立形状直觉
② agent/_agent.py:3091 _next_action —— 整个循环的大脑
③ agent/_agent.py:773 _reply_impl —— 看决策怎么被执行
④ agent/_agent.py:2089 _execute_tool_call —— 五道关
⑤ permission/_engine.py 六步评估,先读 _check_default
⑥ agent/_agent.py:2536 _split_context_for_compression —— 不动点迭代
⑦ app/_bus_ops.py:141 并发交接协议的注释

前四步读完,你已经能讲清楚「AgentScope 的 ReAct 循环凭什么能停能续」。


6.5 总代码地图

按能力找

想找什么文件路径符号名
agent 主循环src/agentscope/agent/_agent.pyAgent_reply_impl_next_action
三种下一步动作src/agentscope/agent/_utils.pyReasoningActingExit
四个配置类src/agentscope/agent/_config.pyContextConfigInjectionConfigReActConfigModelConfig
可存盘状态src/agentscope/state/_state.pyAgentStateReplyContextToolContextTaskContext
消息与块src/agentscope/message/_base.py_block.pyMsgappend_eventToolCallBlockToolCallStateHintBlock
事件协议src/agentscope/event/_event.pyEventTypeReplyEndEventRequireUserConfirmEvent
结束原因src/agentscope/types/_reply.pyReplyFinishedReasonErrorType
工具管理src/agentscope/tool/_toolkit.pyToolkitcall_toolcheck_tool_available
工具协议src/agentscope/tool/_base.pyToolBaseToolMiddlewareBase
工具组src/agentscope/tool/_tool_group.pyToolGroup
内置编码工具src/agentscope/tool/_builtin/BashReadWriteEditGrepGlob
Bash 静态分析src/agentscope/tool/_builtin/_bash_parser.pyBashCommandParser
执行后端src/agentscope/tool/_builtin/_backend.pyBackendBaseLocalBackend
权限src/agentscope/permission/PermissionEnginePermissionModePermissionDecisionPermissionRule
模型src/agentscope/model/_base.pyChatModelBasecount_tokensgenerate_structured_output
格式化src/agentscope/formatter/FormatterBaseOpenAIChatFormatterOpenAIMultiAgentFormatter
中间件src/agentscope/middleware/MiddlewareBaseReplyBudgetControlMiddlewareRAGMiddleware
工作区/沙箱src/agentscope/workspace/WorkspaceBaseLocalWorkspaceOffloader
MCPsrc/agentscope/mcp/MCPClientStdioMCPConfigHttpMCPConfig
技能src/agentscope/skill/SkillSkillLoaderBaseLocalSkillLoader
服务层src/agentscope/app/ChatServiceMessageBusTeamSayAgentCreate
终端交互src/agentscope/console/_console.pylaunch_console

按例子找

例子路径演示什么
终端 agentexamples/console/main.py工作区 + 技能 + 长期记忆 + 卸载的完整组装
服务端examples/agent_service/多租户 FastAPI 服务
Web UIexamples/web_ui/配套前端
长期记忆examples/long_term_memory/ReMe / Mem0 / agentic memory 三种后端
RAGexamples/rag/知识库检索

按测试找行为

tests/ 下有 133 个 *_test.py。想确认某个行为,这几个最直接:

想确认什么测试文件
基本循环tests/agent_basic_test.py
中断语义tests/agent_interrupt_test.py
人工确认tests/hitl_user_confirmation_test.pytests/hitl_mixed_test.py
外部执行tests/hitl_external_execution_test.py
上下文压缩tests/compress_context_test.py
工具结果截断tests/compress_tool_result_test.py
运行时注入tests/agent_injection_test.py
结构化输出tests/agent_structured_output_test.py
Bash 安全判定tests/builtin_bash_test.py
各家格式化tests/formatter_*_test.py