Skip to content
English

从零理解 Agent Core 的设计 ​

Agent Core 解决的是一个执行问题:应用把可用能力、处理规则和结束方式说明白以后,由谁保证这些处理按顺序运行,并遵守预算、取消和输入输出约定?把它落到你的场景里——你负责一个演示服务,同事天天在群里 @ 你问“服务在线吗”。你想做一个服务台 Agent:自动回答,还讲得清“为什么这么答”:答案来自真实查询回执,而不是嘴上说在线。

它不替应用决定“应该经营什么业务”,也不要求每种 Agent 都长成同一个业务流程。你可以先用一个普通节点回答问题,再给同一个流程增加模型、工具、资料和检查。增加需求时,补充相应配置即可。

最笨的办法你已经会写:一段脚本先查状态、再拼一句“服务在线”。可它跑着跑着就露馅,于是你撞见四个麻烦:步骤一多,先后和分支开始乱;一次查到的数据,说不清何时串到了下一次;预算、取消、取结果这些约定没人兜底;真要接模型、资料、工具,又不知道往哪装、怎么才不跟核心绑死。这篇就从这四个麻烦出发,逐一引出对应的对象。完整可运行版本见搭建第一个 Agent App;想先理解执行过程,可以接着读工作机制和原理。

先认识三个对象 ​

从前三个麻烦说起。最笨的脚本里,步骤一多就乱——这是“谁来定义处理单元与去向”;跑完一次的数据不能串到下次——这是“这一次的数据放谁身上”;预算、取消、取结果兜不住——这是“谁保证执行边界”。三个麻烦各有一个对象接住:

对象它回答的问题服务问答中的例子
Workflow有哪些处理单元,它们如何连接,何时结束?读状态 → 组织回答 → 交付
AgentCoreEngine本次如何执行这些定义,并控制运行边界?检查轮次、运行节点、提交状态、选择后继
state这一次运行已经知道什么?用户问题、实际查到的状态、待交付回答

Workflow 是可复用的定义,state 是一次执行的数据。不要把上一位用户的 state 当成下一位用户的初始值,也不要把请求身份或权限保存在共享定义的可变字段里。

先看只有一个节点的版本:

ts
import { AgentCoreEngine, defineWorkflow, route } from '@jangts/agent-core';

type State = { question: string; answer?: string };
const workflow = defineWorkflow<State, string>({
  id: 'service-helpdesk',
  nodes: [{
    id: 'answer',
    description: '回答演示服务状态',
    run: () => ({ answer: '演示服务在线。' }),
    routes: [route.deliver('answer')],
  }],
});
const agent = new AgentCoreEngine({ workflow });
console.log(await agent.run({ question: '演示服务在线吗?' }));
js
import { AgentCoreEngine, defineWorkflow, route } from '@jangts/agent-core';

const workflow = defineWorkflow({
  id: 'service-helpdesk',
  nodes: [{
    id: 'answer',
    description: '回答演示服务状态',
    run: () => ({ answer: '演示服务在线。' }),
    routes: [route.deliver('answer')],
  }],
});
const agent = new AgentCoreEngine({ workflow });
console.log(await agent.run({ question: '演示服务在线吗?' }));

这个节点返回 { answer },表示它想更新的状态字段。route.deliver('answer') 取出该字段,作为这次 agent.run 的结果。这里没有模型,因为这一阶段只学习执行合同;“用了 Agent Core”不等于“每个节点都必须请求模型”。到这里,前三个麻烦有了着落,还差第四个:模型、资料、工具这些能力,怎么装上去又不跟核心绑死。

四种职责,三个 SDK 角色 ​

第四个麻烦登场:模型、资料、工具都是 App 手里的能力,但核心不能反过来长死在某个具体模型或某个资料库里。于是职责被分成三层,另加一个真正的业务方,下面这张图就是它们的关系。

图中的箭头表示装配或调用关系。Core 接收符合接口的实现;它不导入 Support 或 Kit。

Core、Support、Kit 是 SDK 内的职责关系,App 是实际业务的拥有者。它们不是必须串行经过的四层运行引擎。

角色提供什么不替谁做决定
CoreApp 调用 Core、Core 调度 App 的接口,以及执行保证不拥有具体业务事实和权限规则
Support对 Core 合同的推荐实现、策略补全和配置生成不要求 Core 依赖它,不新增平行调度协议
KitApp 可选的局部业务库,例如知识选择、证据问答、产物和持久任务不把自己的字段变成所有 Agent 的必填项
App真实依赖、身份与授权、能力清单、事实、业务分支和存储政策不靠模型文字推断某个副作用已经成功

例如,要让模型根据真实状态回答:App 提供查询状态的服务和授权规则;Support 可帮助组织工具的 schema、注册和模型能力投影;Core 负责执行调用、传递取消并校验返回;若还需要按章节选知识,App 再选择使用 Knowledge Kit。

不使用 Knowledge Kit 的 Agent,不必为了接入 Core 而虚构文档、章节或证据字段。

Core 接收合同的实现 ​

分好职责后,紧接着的问题来了:你手里的能力,以什么形状交给 Core?答案是一套统一合同,而不是某种固定写法。

一个参数是否可用,由它满足的 Interface/type 决定。普通对象是实现方式之一;类实例也可以,只要满足同一合同。Support 的产物可以直接传给 Core,无须先“转成普通对象”。

下面两个离线 Provider 提供相同的流接口,均可作为模型绑定里的 provider。它们只用于观察调用过程,不具备真实的语言理解能力。

ts
import type { ChatProvider, ModelMessage, ModelProviderContext } from '@jangts/agent-core/types';

const objectProvider: ChatProvider = {
  async *stream(_messages, context) {
    context.signal.throwIfAborted();
    yield '演示服务在线。';
  },
};

class OfflineProvider implements ChatProvider {
  async *stream(_messages: readonly ModelMessage[], context: ModelProviderContext) {
    context.signal.throwIfAborted();
    yield '演示服务在线。';
  }
}
const classProvider = new OfflineProvider();
js
/** @type {import('@jangts/agent-core/types').ChatProvider} */
const objectProvider = {
  async *stream(_messages, context) {
    context.signal.throwIfAborted();
    yield '演示服务在线。';
  },
};

class OfflineProvider {
  async *stream(_messages, context) {
    context.signal.throwIfAborted();
    yield '演示服务在线。';
  }
}
const classProvider = new OfflineProvider();

这不表示任意形状都能通过运行时检查。Provider 必须有可调用的 stream,消息和分片仍遵守模型协议,Core 也继续校验预算和结果。对确实要求 Core 所有构造器的参数,以具体 API 的身份要求为准,不能仅复制几个字段冒充该对象。

简化来自缺省、复用和代指 ​

能力能装上了,你又撞见一个新麻烦:每加一个需求,都要重复配一遍关系。真正的简化不在“省几个参数”,而在少决定、少重复。

学习接口时,更有价值的简化是少决定一些事、少重复配置一些关系。

方式具体做法后续如何增加需求
缺省省略 Workflow 的入口时使用首个节点;最小 Agent 不必先配完整模型系统需要非首节点入口,再补 entryNodeId
复用Workflow、Provider、工具定义在适当生命周期内准备一次修改已有定义或不可变组合,继续使用相同运行接口
代指工具已绑定后,节点按 toolId 调用;多个模型登记后按 ID 选择增加登记项,再在确有差异的调用中指定 ID

例如,Provider 绑定到 Agent 以后,节点使用 context.model。节点不用再逐层传 Provider、模型预算、当前诊断父节点等固定关系。请求通常只提供这次变化的 state 和取消信号。

构造时已经知道 Workflow,可以直接传入;需要稍后装配,则先省略,再调用一次 bindWorkflow。两种方式都建立 Agent 与 Workflow 的一对一绑定。

Core 的缺省与 Support 的推荐不同 ​

缺省和推荐听起来像一回事,其实回答两个不同的问题。

Core 的缺省解决“缺少这个参数时怎样运行”。Support 的推荐要回答“在这个明确场景中,怎样配置更合适”。

例如,Core 的模型调用上限不是供应商容量承诺,也不是每个业务的推荐额度。Support 的场景策略可以结合任务、已知模型能力和保留要求,生成更适合的参数或处理节点;精确数值和自定义实现仍应能够覆盖这些建议。

Support 可按快速答复、资料问答、受限上下文等场景提供不同策略,并允许精确覆盖参数。各策略都通过相同的 Core 接口执行。

Workflow 把业务关系留在一起 ​

回到服务台:光会回答不够,得先读到真实状态,再组织回答。“数据不能串位、步骤要分先后”在这里变成两个节点加一条去向。

当服务问答需要读取工具再组织回答时,可以在原定义里增加节点和路由:

text
lookup:取得真实状态
   ↓
answer:根据状态组织回答
   ↓
deliver:返回已经检查的回答

节点负责一个处理单元,路由说明去向,Engine 负责执行。入口只负责装配和请求边界,不需要知道“查到离线状态该走哪条业务分支”。

节点内部使用普通函数组织计算。需要独立调度、复用、观察或检查的处理单元,可以定义为单独节点。

route 函数生成原始路由配置 ​

节点有了,去向怎么表达?“离线走这条、在线走那条”这件事,就是路由。

原始路由对象就是完整的公开配置。你可以手写,也可以使用 route/route.xxx 固化常用判断、表达语义并生成相关标识。

ts
import { route } from '@jangts/agent-core';

const handwritten = {
  id: 'status-online',
  conditionId: '{nid}.{rid}',
  when: (state: { service: { status: string } }) => state.service.status === 'online',
  targetNodeId: 'answer',
};
const generated = route.toIfEqual('answer', 'service.status', 'online');
js
import { route } from '@jangts/agent-core';

const handwritten = {
  id: 'status-online',
  conditionId: '{nid}.{rid}',
  when: state => state.service.status === 'online',
  targetNodeId: 'answer',
};
const generated = route.toIfEqual('answer', 'service.status', 'online');

两段使用相同的相等判断和目标,生成的标识未必与手写标识相同。Workflow 对手写和生成的路由使用相同的校验与标识处理;生成器分别提供自己的默认命名。

conditionId 中的 {nid}、{rid} 是原始配置可用的占位符。Workflow 会在知道节点和最终路由 ID 时求值。toIfEqual、toIfEqualWith 支持点分路径,例如 items.0.status;其他方法是否接收路径,要看它自己的签名,不能把“字段”一律理解为嵌套路径。

route.to* 一般根据目标或判断内容生成标识,这是各专用函数的习惯。需要稳定的外部关联标识时可以显式覆盖。重复 ID 的处理和具体模板见对应接口页。

扩展定义,保持运行方式 ​

服务台不会永远只有“查状态”一件事。加复核、加解释时,别把已经跑通的流程推翻重来。

Workflow 的 add 生成新 Workflow,旧实例不变。它适合组织共同基础上的不同 Agent:共用查询流程,再分别加复核、解释或交付节点。新 Agent 可以直接使用组合后的定义。

fork 从当前 Agent 产生的内存 checkpoint 创建分支。省略 Workflow 时继承已有定义,传入时在内部用 add 组合。应用依赖通过 resume 显式提供。

CoreStep 支持过程式自编排,由应用决定每一步返回 continue 或 complete。流程需要集中展示节点和路由关系时,可使用 Workflow。

过程监听与界面更新 ​

同事在群里问完,还想看到“正在查询、正在整理”。你不想让 Core 去碰界面,那就让它只报事实,App 自己决定显示什么。

App 注册执行监听器,就能在任务完成前收到模型、工具、节点等事实。App 决定哪些事实转成“正在查询”“正在整理”等界面状态。Core 不操作 App UI,也不要求 App 轮询进度。

节点可以报告自身的 usedModel 标记、业务 payload 和子日志 subs。子日志通过执行上下文的 createSubFlowLogger() 创建,先用 write 记录计划,再用 fix 补充结果;执行身份、时间和关联由 Loop 提供。usedModel 未报告时保持未知,不自动汇总子步骤。旁路观察与最终内容交付有不同保证:观察失败不会改变业务执行;需要等待的内容 sink 按顺序消费并保留背压。

最后,定义、执行事实和业务授权始终分别看待。定义说“可以查询服务”,不证明某次查询已经发生;日志说“模型提议调用”,不证明工具已经成功;最终文案说“已完成”,也不能代替真实回执。后续的机制文章会沿一次运行解释这些边界。

模拟模型也是可选 Kit ​

动手前想离线跑通,就把模型换成可复现的假模型,其余一路不变。

MockGPT用来准备可重复的回复和故障场景。App 可以直接调用 generate,也可以把 fetch 注入普通 Provider;Core 执行器不依赖模拟器,工具授权、预算和完整解析仍在原来的位置。

ts
import { MockGPT } from '@jangts/agent-core/mock';
const mock = new MockGPT({ rules: [{ prompt: '服务状态', generator: { online: true } }] });
console.log(await mock.generate({ model: 'demo', messages: [{ role: 'user', content: '服务状态' }] }));
js
import { MockGPT } from '@jangts/agent-core/mock';
const mock = new MockGPT({ rules: [{ prompt: '服务状态', generator: { online: true } }] });
console.log(await mock.generate({ model: 'demo', messages: [{ role: 'user', content: '服务状态' }] }));

固定结果适合严格回归,输入相关生成器适合探索交互。两者都不替代真实模型的语义效果验证。

接口参考、使用示例与开发指南。