CyberStrikeAI Agent 优化:解耦的不是「工具」,是「时间」
以前 Agent 调一次 nmap / sqlmap,整轮迭代就得干等到工具结束。
现在工具丢给后台 worker 跑,本轮只等待有限时间;超时返回execution_id,Agent 可以先继续推理,结果后续再读,需要时再 wait / cancel。
0. 这套机制实际做到了什么
先划清边界。它不是:
LLM 在等 nmap时还能继续「边想边跑」一轮里自动并行调度任意多个阻塞工具
它实际做到的是:
工具调用对 Eino/模型仍是同步 tool call(协议兼容)。 阻塞劳动下沉到 cancellable worker,带显式 execution_id。Agent 对本轮调用只做 有界 Wait( tool_wait_timeout_seconds)。软等待到期后:本轮 tool result 返回(含句柄),下一轮迭代可以继续;worker 可继续跑。 后续用 wait_tool_execution/get_tool_execution/cancel_tool_execution观察或终止。
一句话:
解耦发生在「软超时之后的跨轮决策」:软等待内,当前轮停在等工具,下一轮还开不了;软超时返回句柄后,本轮才能结束,后续迭代才能继续,后台任务则接着跑。
这是当前阶段在同步 tool-call 约束下的务实落点,不是终点;后面会围绕调度、超时语义和结果回灌继续打磨。
一、问题本质:串行不只是慢,还会把控制平面绑死
经典 tool-calling 循环:
思考 → 调工具 → 等结果 → 再思考 → …
对短 API 助手够用;对红队 Agent,工具耗时分布是重尾的:
此时把 max_iterations 调到 12000 帮助有限:中间一次数十分钟阻塞,后面本可发生的决策全部排队。
麻烦往往不只是「慢」,而是四类连锁问题:
决策窗口被占死:扫描未完成前,模型无法换路、写 Fact、开短工具。 取消语义绑死:停「等待」还是停「执行」分不清。 上下文与存储双爆:巨大 stdout 进 messages + DB,续跑再灌一次。 多会话互相伤害:一个坏 MCP server 拖垮全局。
所以正确命题不是「超时调大」,而是:
工具的物理耗时,不该定义 Agent 控制平面的阻塞时间;但协议层仍必须给出可推理的 tool result。
二、三条常见捷径为什么不行
1. Fire-and-forget
返回「已提交」就结束。渗透场景里「没结果」极易被模型幻觉成「没洞」。因果链断了,比慢更危险。
2. 事件回调推进下一轮
与 Eino ADK / OpenAI-compatible tool-calling 假设冲突:通常要 本轮 tool result 回来,才进入下一轮 model call。硬改事件驱动等于重写 runtime。
3. 无限延长超时
只是把假死合法化。runner 仍被占着,用户无法插话改策略。
可行解必须同时满足
对模型:同步 tool call 对 runner:有界等待 对执行:可延续生命周期(超时 ≠ 默认杀进程) 对系统:可取消、限流、熔断、输出上限、恢复一致
CyberStrikeAI 的答案是:ExecutionService + 控制面工具 + 结果治理 + 取消矩阵。
三、架构主轴:同步门面 × 异步执行
核心注释(internal/mcp/execution_service.go):
ExecutionService keeps Eino-facing tool calls synchronous while moving the untrusted blocking work into cancellable workers with explicit execution IDs.
3.1 主路径(当前生产路径)
工具链路是:
Eino ToolsNode
→ einomcp.mcpBridgeTool.InvokableRun
→ Agent.ExecuteMCPToolForConversation
→ mcp.Server.CallTool / ExternalMCPManager.CallTool
→ ExecutionService.Submit + Wait(soft)
einomcp 还要处理一个协议缝:Eino 工具通道主要返回字符串,于是用
__CYBERSTRIKE_AI_TOOL_ERROR__\n + body
把 MCP 的 IsError 传到上层,再在 eino_adk_run_loop 里还原为 success/isError。软超时结果会带这个前缀——模型侧仍看到“错误通道”,但 UI 会另行识别。
3.2 CallTool 的两段式
以内部 MCP 为例(外部 MCP 同构,多一个 PreRun 抢槽):
Submit(run=真实 handler)
→ 落库 queued,启动 worker
Wait(ctx, id, tool_wait_timeout)
→ 完成:返回真实 ToolResult
→ ErrExecutionWaitTimeout:返回「后台仍在跑」文案 + execution_id
Worker 侧:
runCtx := context.WithoutCancel(ctx) // 关键:剥离父 ctx 的 cancel/deadline
这意味着:Wait 结束、甚至 Wait 的 ctx 取消,都不自动等于 worker 被杀。
生命周期分离是刻意的;否则「软超时后继续后台跑」不成立。
3.3 状态机:库状态 vs 展示状态
ExecutionService / DB 真实状态:
queued | |
running | |
completedfailed / cancelled / hard_timeout / orphaned |
前端展示态 background_running:
不是库字段,而是 eino_adk_run_loop 在推 tool_result 时,若判定为「MCP 后台等待结果」,写入 SSE:
status=background_runningUI isError=false(不标红)同时保留 modelFacingIsError=true(模型仍走错误通道提示)
判定函数 isMCPBackgroundWaitResult 要求同时具备:
含 execution_idstatus 为 running/queued 含软等待文案信号(「工具已提交到后台执行」/ wait_timeout/ still running 等)
这是典型的 双通道语义:给人看的状态,和给模型的激励信号,故意不完全相同。
四、双时钟:把「等待」和「处决」拆开
很多人以为只有一个 timeout。实现里至少有多层时间边界,职责不同。
4.1 软等待:tool_wait_timeout_seconds
定义:本次 Agent-facing tool call 最多阻塞多久。 到期:返回句柄,worker 继续。 默认/示例:代码默认常见 60s; config.example.yaml可到 300s;文档建议长任务场景 30–60s 起步,不建议靠把软等待调成数分钟来“解决”长任务。0:等到完成(退回旧世界)。
4.2 Agent 包装的 tool_timeout_minutes
Agent 在调用 MCP 前可能:
toolCtx, cancel = context.WithTimeout(ctx, ToolTimeoutMinutes)
它主要约束的是 CallTool 里的 Wait 所使用的 ctx。
但 Submit 后 worker 用 WithoutCancel(toolCtx),deadline 被剥离。
因此:
DeadlineExceeded | 仍可能继续跑 | |
ExecutionRequest.HardTimeout 字段在 ExecutionService 里存在,且可映射到 hard_timeout 状态;但当前 Server.CallTool / ExternalMCPManager.CallTool并未传入 HardTimeout。
也就是说:“硬超时杀后台”不是靠这个字段自动完成的,而是靠会话取消、cancel_tool_execution、exec 空闲超时、工具内部逻辑等。
配置说明里值得写清楚:不要以为配了 tool_timeout_minutes,后台 sqlmap 就一定在 60 分钟被杀。
4.3 其他边界
shell_no_output_timeout_seconds:防 exec 静默挂死外部 MCP 并发槽 + 熔断:防坏依赖放大 orphanedreconcile:进程没了,DB 不能永远 running
五、控制面工具:把生命周期操作变成可推理原语
仅返回 execution_id 不够;模型需要一等工具:
get_tool_execution | |
wait_tool_execution | |
cancel_tool_execution |
5.1 防套娃:控制工具的外层软等待为 0
isExecutionControlTool 为真时,CallTool 把外层 waitTimeout 置 0。
否则会出现:wait_tool_execution 自己又被 30s 外层软超时打断——语义崩坏。
5.2 「wait 超时」≠「目标失败」≠「本次 wait 失败」
单测钉死了一件事:当 wait_tool_execution 返回「目标仍 running + 本次等待到达」时:
该次 wait execution 在服务侧记为 **completed**模型正文仍可保留 IsError(提示“别当最终结果”)前端不应标红成工具失败
这是训练信号治理:如果把「还没跑完」记成失败,模型会学到错误策略(乱 cancel / 重复开扫)。
5.3 模型仍可能把自己堵回去
系统给的是能力,不是调度器。若模型一拿到 execution_id 就:
wait_tool_execution(timeout_seconds=600)
迭代时钟会再次被占满。
更稳妥的用法是:短 wait + 穿插其他工作 + 必要时 cancel,而不是把软超时省下来的时间,立刻用长 wait 还回去。
六、取消矩阵:产品语义比 cancel() 更难
实现上取消入口并不止一个,意图也不同。
CancelTask(ErrTaskCancelled)ToolCanceler | AbortActiveEinoExecute) | ||
CancelTask(ErrInterruptContinue) | 否 | ||
CancelToolExecutionWithNote | |||
cancel_tool_execution |
「中断并继续」不批量杀工具,是为了保住半小时扫描,让后续轮次还能 wait_tool_execution。
这不是边角;这是长任务产品能不能用的分水岭。
另外还有平行路径:
MCP: ExecutionService+ToolRunRegistryEino filesystem execute:EinoExecuteRunRegistry(专门为 amass 等长命令的中断说明合并)
文章若只谈 MCP,会漏掉真实系统里的第二条生命线。
七、外部 MCP:把不可信依赖关进隔离区
外部 MCP 的 CallTool 在 PreRun 里抢槽:
per-server semaphore(默认 2) global semaphore(默认 16) 连续失败熔断(阈值 + 冷却)
测试验证过关键不变量:
软超时后,调用方带着 execution_id返回;即便第二调用还在 queued抢槽,也不会把 Agent Wait 无限堵死;取消 Wait 的 ctx 后,worker 仍可跑完并进入 completed。
这解决的是第二类串行:排队时间也不该绑架迭代。
八、结果治理:异步之后,真正会炸的是上下文
路径:
完成 → NormalizeToolResultForStorage → 内存 execution → DB → 返回 Agent
目标是 canonical result:模型、监控、get/wait、续跑尽量同一份兜底后正文。
Eino 侧还有一层展示一致性:
ADK tool_result推送以 reduction 后、进 ChatModel 的正文为准;避免 ToolInvokeNotify 在 reduction 前抢先推全量输出,造成 UI/监控与 agent 所见不一致; UpdateMCPExecutionDisplayResult把监控展示同步成模型可见正文。
续跑使用 LastAgentTraceInput(model-facing trace),并在恢复入口再次裁剪历史超大 tool 内容——覆盖升级前脏数据、手工导入、阈值调低等现实情况。
对长程渗透,这决定你能不能「后台扫十分钟,上下文只留句柄与摘要」。
九、和渗透心智的对齐:状态空间搜索需要「可调度的时间」
Skill 体系把渗透看成状态空间搜索:路径从黑板上的已验证事实涌现。
旧串行模型把搜索退化成单线程流水线。
新模型允许更接近指挥节奏的策略:
发起长扫描(软超时拿 execution_id)
→ 写 Fact / enrichment / 换路 / 短工具
→ 短 wait_tool_execution 探进度
→ 完成则吸收;未完成则继续开拓其他 frontier
→ 价值下降则 cancel,释放目标与本地资源
再次强调边界:这是 跨轮重叠,不是完整的 multi-inflight tool futures runtime。
但对 Eino 同步语义,这是务实切面:先拆掉最大痛点(无限阻塞),再谈更激进的并行编排。
十、落地建议(配置 × 产品 × 提示词 × 运维)
10.1 配置怎么配
长任务场景建议从这里起步,再按实测调:
agent:
max_iterations:800# 太大等于放弃循环保护
tool_timeout_minutes:60# 主要约束 Wait/调用包装,不等于自动硬杀一切后台
tool_wait_timeout_seconds:30# 让长任务尽快交还迭代权;别指望靠调大它“完成扫描”
external_mcp_max_concurrent_per_server:2
external_mcp_max_concurrent_total:16
external_mcp_circuit_failure_threshold:3
external_mcp_circuit_cooldown_seconds:60
shell_no_output_timeout_seconds:1200multi_agent:
eino_middleware:
reduction_enable:true
reduction_max_length_for_trunc:50000
经验法则:
tool_wait_timeout_seconds | ||
tool_timeout_minutes | ||
10.2 Agent 策略(提示词 / skill 应强调)
长工具软超时后:先决策是否值得继续等,不要反射性长 wait。 wait 用短脉冲(如 15–60s),中间穿插其他假设检验。 明确区分: failed/cancelled/仍 running。用户「中断并继续」后:优先 get/wait已有execution_id,不要无脑重开扫描。价值下降或目标变更:主动 cancel_tool_execution并写负结果 Fact。
10.3 产品交互
后台运行用 background_running展示,不标红。停止任务 vs 中断并继续:取消矩阵必须对用户可感知。 监控页提供单 execution 终止,并带回 note 合并进工具结果。
10.4 运维与正确性
启动 orphanedreconcile,避免幽灵 running。观察熔断打开频率:过高说明外部 MCP 质量或并发策略有问题。 抽查 DB 与 agent 正文是否同一份 capped result。 用固定对话回归:
调用 exec 执行 sleep 120;超过 10 秒未完成则回报 execution_id;
再 wait_tool_execution 5 秒;仍未完成则 cancel,并说明最终状态。
预期:首轮后台句柄、wait 超时不标红失败、cancel 后状态自洽。
十一、深度对照:优化前后时序
旧世界
Agent ──[阻塞 40min 等 nmap]──────────────────────► 下一轮思考
Tool ████████████████████████████████████████████
新世界(软等待=30s)
Agent ──[Wait≤30s]──► 得 execution_id ──► 短工具/写Fact/再决策 ──► wait/cancel
Tool ████████████████████████████████████████████(可并行于后续迭代)
▲
软超时只交还迭代权,不默认杀 Tool
你省下的不是 nmap 本身的 CPU,而是 决策密度 与 尾延迟隔离。
十二、已知边界,也是下一步方向
软等待内仍阻塞当前 tool call;解放的是后续迭代。 **HardTimeout字段未接到 CallTool Submit**;勿高估tool_timeout_minutes对后台 worker 的处决力。**background_running是 UI/SSE 展示态**,不是 DB 枚举值。模型侧软超时走 IsError/ToolErrorPrefix;依赖提示词与控制工具描述,弱模型可能误判。execute 与 MCP 是平行生命周期;取消与中断路径不完全共用一套对象。 外部 MCP 内部缓冲不可控;本系统只能在入口后兜底与限流。 超长工具输出会先落盘再截断:全文写入 tmp/reduction/.../trunc/<execution_id>(或reduction_root_dir),上下文/DB 只保留带路径的<persisted-output>预览,可用read_file回读。没有自动最优调度器;长 wait 仍可把收益吐回去。
这些是当前契约,也是持续探索的入口:例如更完整的硬超时接线、execute/MCP 生命周期统一、结果完成唤醒与回灌、以及更稳的多工具并行编排。平台先把「不可能」变成「可编排」;下一步是让编排更省心、更少依赖模型自觉。
十三、若只记住一张契约图
控制平面(Agent / Eino)
同步 tool call
有界 Wait
跨轮继续推理
│
│ execution_id
▼
调度平面(ExecutionService)
queued → running → terminal
Wait / Cancel / OnDone
│
▼
数据平面(worker / 外部 MCP / execute)
不可信阻塞劳动
│
▼
治理平面
输出上限 · 并发 · 熔断 · orphaned · 取消矩阵 · 双通道展示
优化的本质不是多几个异步 API,而是完成角色分离:
Agent:决策与调度 ExecutionService:生命周期与观察 Worker:劳动 Governance:唯一真相与故障边界
十四、结语
评测榜上的 Agent 比「题能不能做对」;生产环境里的安全 Agent 比:
长工具会不会把会话焊死 用户能不能中途改口且不误杀扫描 多插件会不会互相拖垮 两小时作战会不会撑爆上下文与数据库 重启后世界是否仍自洽
CyberStrikeAI 这轮优化给出的阶段性答案是:
不要让工具的物理耗时,定义 Agent 的思维节拍。
给执行一个 ID,给等待一个上限,给取消一张矩阵,给结果一份 canonical 真相——然后把下一轮思考的权利,交还给迭代循环。
在同步 tool-call 约束下,这已经是比较稳的务实方案;距离更理想的 Agent 运行时还有空间。我们会继续沿着真实渗透场景打磨:超时与取消语义、结果回灌、多工具编排,以及模型和 skill 更好用的默认策略。欢迎一起验证、拍砖、共建。
代码索引(便于对照本文)
WithoutCancel | internal/mcp/execution_service.go |
internal/mcp/server.goCallTool | |
internal/mcp/external_manager.go | |
internal/mcp/execution_control_tools.go | |
internal/einomcp/mcp_tools.go | |
background_running | internal/multiagent/eino_adk_run_loop.go |
internal/handler/task_manager_tool_cancel_test.go | |
internal/mcp/run_context.gointernal/handler/task_manager.go | |
internal/mcp/tool_result_guard.go | |
internal/monitor/reconcile.go | |
docs/zh-CN/tool-execution-governance.md |