2026年8月14日 2 分钟阅读

不想把 Agent 逻辑锁进 SDK:用 Substructure 把配置、运行与人工接管拆开

tinyash 0 条评论

许多团队做内部 Agent 时,第一步往往是选一个 SDK,然后把模型调用、工具定义、会话状态和业务判断写进同一个服务。原型很快,但系统一旦接入 Slack、MCP、长任务或人工审批,问题会变得具体:一次重启是否丢掉会话?工具凭据由谁持有?业务系统如何在模型决定执行之前插手?不同语言的后端是否必须各自接一套 Agent 框架?

Substructure 提供的是另一种分层方式。它是一个可自托管的 Agent 引擎:引擎负责执行 Agent 循环、保存每一步、流式输出事件,并处理模型与 MCP 连接;业务代码则可选地作为 HTTP worker,在决策节点接收 JSON、返回下一步动作。项目的声明写在 substructure.toml,而不是散落在某一种语言的 SDK 调用里。它的源码仓库使用 Functional Source License 1.1,许可证文本说明会转换为 Apache 2.0,因此在部署策略上应先按该当前许可证评估,而不是笼统视作宽松许可的开源依赖。

这篇文章聚焦一个实际场景:把告警或运维请求交给 Slack 中的 Agent,同时让团队保留对工具调用、模型选择和人工中断的工程控制。

先划清边界:引擎不是业务工具箱

Substructure 的关键不是“替你写业务逻辑”,而是把通用的运行时职责从业务服务中抽离。官方文档将系统分为三部分:Engine 运行模型、工具、持久化与事件流;Worker 是团队自有的 HTTP 代码;Client 可以是 Slack、浏览器、后端或 CLI。

这意味着 worker 不必承担保存会话、向前端推流或重连恢复等运行时工作。反过来,引擎也不应被赋予业务系统的全部权限。比较稳妥的做法是:引擎保存执行上下文;worker 只暴露经过身份校验和参数校验的业务动作;真正敏感的变更由业务 API 或审批系统完成。

这种边界对多语言团队尤其有用。worker 通信是 HTTP 和 JSON,而非特定语言的对象模型:现有 Python、Go、Java 或 Node 服务都能承接。不要把“没有 SDK”理解成“不需要契约”:请求体字段、认证、超时、重试语义以及允许的动作类型,仍应当由团队版本化和测试。

从一个最小 TOML 项目开始

官方 Quickstart 使用 @substructure.ai/cli 提供的 subs 命令。下面的配置定义一个在 Slack 中响应提及的 on-call Agent;模型提供商与 Agent 的引用名称均由 TOML 显式给出。

name = "oncall-bot"

[llm.openrouter]
type = "openrouter"

[agent.oncall]
llm = "openrouter"
model = "anthropic/claude-sonnet-4-5"
system = "You are the on-call assistant. Summarize evidence before proposing an action."

[slack]
dm = "oncall"
mentions = "oncall"

[remote]
url = "https://api.substructure.ai"

安装和初始化可按官方流程执行:

npm install -g @substructure.ai/cli
subs login
subs apply
subs llm set-key openrouter
subs slack connect

subs apply 会根据配置创建项目;随后再配置 LLM 密钥并连接 Slack。配置文件中应只保存引用和结构,不要写入 API key。仓库 README 也明确区分了两种密钥路径:可让引擎代为调用模型,或让 worker 自己完成模型调用,使引擎不接触该密钥。后者适合已有统一密钥代理、审计或地域隔离要求的团队。

先在一个权限很小的 Slack 频道试运行。把 system prompt 限制为“收集证据、提出建议”,不要直接允许变更生产环境。只有当日志查询、工单创建和权限边界都验证清楚后,再逐步加入可执行工具。

用 worker 在每个决定点接管,而非重写整个循环

当 Agent 只需回答问题,TOML 配置就够了。需要把某些调用改为自有工具、替换模型,或在危险动作前暂停时,为指定 Agent 增加 worker

[agent.oncall]
llm = "openrouter"
model = "anthropic/claude-sonnet-4-5"
worker = "https://ops.example.internal/agent"

官方说明的模式是:引擎向该 endpoint 发送当前触发器和拟议动作(proposal);worker 可以原样返回 proposal,也可以返回替代动作。例如,文档中的 Node 示例在触发器类型为 tool.execute、工具名为 get_time 时,返回一个 tool.result 动作;其它情况接受原拟议动作。

function decide({ trigger, proposed }) {
  if (trigger.type === "tool.execute" && trigger.name === "get_time") {
    return {
      actions: [{ type: "tool.result", result: new Date().toISOString() }]
    };
  }
  return proposed;
}

生产实现不要直接照搬这个最小示例。更有价值的是把它变成决策关卡:对写操作核验工单号、环境和操作者;对超出额度或影响面较大的动作改为等待审批;对不符合策略的参数返回安全的拒绝或要求补充证据。这样做的重点不是让 worker“比模型聪明”,而是让模型的提议穿过可测试、可审计的业务规则。

一个常见失败模式是把所有工具都塞进 worker,然后把 worker 做成第二个 Agent 框架。结果是会话恢复、流式协议和工具调度又重新分叉。更好的边界是:只有需要组织特定决策的点才进入 worker;普通模型回复、标准 MCP 调用和运行时持久化仍交给引擎。

MCP、持久化与人工中断如何配合

Substructure 可以在配置中声明 MCP server。以官方文档中的 Sentry connector 为例,Agent 只引用 connector 名称:

[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"

[agent.oncall]
mcp = ["sentry"]

再运行 subs mcp login sentry 完成连接授权。文档称引擎会处理授权、读取服务端提供的工具并执行调用,业务 worker 无需直接持有 connector token。这并不等于自动获得最小权限:MCP 服务端账户本身仍应采用只读角色、项目范围限制和独立审计。

运行时层面,官方描述每一步会在执行前保存,因此进程崩溃、重新部署或客户端重连后能够续跑;重复提交同一消息时也应只执行一次。对于“等待人工”这一类流程,Agent 可暂停至用户批准再继续,等待期间不占用计算资源。这个特性适合把“查到告警证据”和“执行修复”拆成两段:前者可自动完成,后者必须在 Slack 或组织自己的审批入口中确认。

但持久化不是万能保险。外部工具若本身不具备幂等性,重放或网络超时仍可能引发重复副作用。因此工具协议应接收业务幂等键,worker 应记录请求和最终决议;在写库、发券、修改基础设施前,必须由目标系统做最终去重,而不能只依赖 Agent 引擎的会话语义。

本地先验证,再把同一份声明部署出去

去掉 [remote] 后,官方文档允许在本机用自己的 key 运行相同配置。常用的验证组合是:

subs serve -c substructure.dev.toml
subs run --agent oncall -o pretty "what is broken?"
subs doctor

subs serve 启动本地 HTTP 服务,subs run 在终端运行指定 Agent,subs doctor 用于检查项目还缺少哪些设置。建议把 substructure.dev.toml 与线上文件分开:开发环境连接测试 Slack 工作区、只读 MCP 和模拟 worker;线上环境再使用受控的 connector、密钥来源和域名。

验收也应分层。先验证配置是否能创建项目、Slack 路由是否落到正确 Agent;再用固定输入验证 worker 对允许、拒绝和人工暂停三种结果的处理;最后故意在工具调用中断后重启服务,检查恢复后是否仍保留同一个业务幂等键。仅看到聊天回复正常,不足以说明长任务、审批和副作用都可靠。

还应为 worker 建立可回放的契约测试:保存一组脱敏的触发器与 proposal,断言同一输入始终得到预期的接受、替换或暂停结果;对超时、无效 JSON 和下游工具返回错误分别规定保守行为。线上观测至少关联会话 ID、业务请求 ID、审批记录和目标系统的幂等键。这样发生争议时,团队能区分“模型提出了什么”“worker 放行了什么”与“外部系统实际执行了什么”,而不会只剩 Slack 对话作为证据。

何时适合采用

Substructure 适合已经有业务服务和工具体系、但不想在每个语言栈重复实现 Agent 运行时的团队。它特别适合把 Slack、MCP、长任务、审批和自有 HTTP 逻辑组合为同一个项目声明的场景。其价值来自“运行时统一、决策仍可外置”,而不是替代现有权限系统。

如果需求只是单次脚本生成或一个没有工具调用的聊天页面,引入独立引擎未必划算;如果团队需要完全宽松的许可证或稳定的 1.0 协议,也应注意项目当前处于 pre-1.0,接口和 wire protocol 可能在发布间变化。对大多数内部自动化项目,最可控的起点仍是一个只读 Slack Agent:先让它产出可核查的证据和建议,再逐步把少量、可回滚的动作接入 worker 与审批链。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。