跳转到正文

通过 Loop 组合 Graph ​

English · Runtime API · RAG 应用

Graph 描述单个阶段内的节点与依赖。Loop 组合一个或多个 Graph,控制执行顺序、分支、重复和停止。Graph 不包含子 Graph 或内嵌 Loop 节点。

两种 Loop 定义 ​

已有 loop({ graph, bind, update, done, maxIterations }) 适用于显式状态机:每轮选择一个 Graph,绑定输入,通过输出更新状态并判断停止。graph 可以是固定 Graph,也可以是根据状态选择 Graph 的函数。此调用保持兼容。

阶段输入输出类型不同、需要复用检查点和恢复分支时,使用执行计划:

ts
import { createDitto, graph, graphStep, loop } from "@codesoul-co/ditto/runtime";
import { createContextWorker, createInMemoryContextStore } from "@codesoul-co/ditto/worker/context";

// 本例仅演示编排契约,显式使用测试 Context;完整 Agent 使用 Redis。
const runtime = createDitto({ workers: [createContextWorker({
  services: { stateStore: createInMemoryContextStore() }
})] });
const load = graph<{ question: string }>("load-question")
  .node("context", "CONTEXT.LOAD", [], input => ({
    sources: [{ id: "question", content: input.question }]
  }));
const select = graph<{ context: import("@codesoul-co/ditto/contracts").Context }>("select-context")
  .node("selected", "CONTEXT.SELECT", [], input => ({
    context: input.context, purpose: "infer", limit: 1
  }));
const workflow = loop({
  id: "question-workflow",
  maxIterations: 4,
  plan: function* (input: { question: string }) {
    const loaded = yield* graphStep(load, input);
    const result = yield* graphStep(select, { context: loaded.context });
    return result.selected.context;
  }
});
try {
  const result = await runtime.loop(workflow, { question: "设备如何借用?" }, {
    onGraph: event => console.log(event.graphId, event.status)
  });
  console.log(result);
} finally { await runtime.close(); }

执行计划是同步生成器。yield* graphStep(graph, input, options?) 把 Graph 和输入交给 Loop;Loop 调用 Runtime 执行 Graph,完成后把类型正确的输出送回计划。计划本身不调用 runtime.run(),不直接运行 Worker,也不执行模型、数据库、网络或文件操作。

if 根据已完成的输出选择下一 Graph,for 可重复交出同一 Graph。真正的 Graph 执行、执行次数限制、取消、Worker 分配和运行生命周期归 Loop / Runtime 管理。一个可复用的 function* 子计划可以用 yield* childPlan() 接入同一个 Loop;它不会另启 Loop,也不会重置总执行预算。

公开类型与方法 ​

API说明
GraphPlan<R>只交出 GraphInvocation、最终返回 R 的同步生成器
graphStep<I,O>(graph,input,options?)输入按 Graph 的 I 校验,恢复值按 O 推导;options 为 GraphRunOptions
LoopPlanDefinition<I,R>{id, plan:(input:I)=>GraphPlan<R>, maxIterations?}
runtime.loop(definition,input,options?)执行计划,返回 Promise<R>
LoopRunOptions.workers按 Graph ID、节点 ID 指定 Worker,保持现有接线形式
LoopRunOptions.onGraph计划型 Loop 的动态 Graph 事件:loopId、iteration、graphId、nodes、status

status 为 started、completed、failed。iteration 从 0 开始;一次实际 Graph 执行占一次预算,失败的执行同样计数。completed 表示 Graph 执行返回,不替代各节点结果中的业务成功校验。事件是实际运行轨迹,不能当作提前穷举所有分支的静态 DAG。观察回调应只记录轻量数据;抛异常会终止当前 Loop。

未指定 maxIterations 时使用 Runtime 的 loopMaxIterations。graphStep 的 concurrency 可控制本次 Graph 内的节点并发;每个计划同一时刻交出一个 Graph,独立节点并行由 Graph 表达。

错误与取消 ​

Graph 抛出的异常会回传到对应的 yield* graphStep,计划可以只捕获允许恢复的错误,例如 Context 缓存不存在;连接失败不能当作缓存缺失。NodeResult.status === "failed" 仍是节点返回值,应用需要检查它,而不是假设所有业务错误都会抛出。

传给 runtime.loop(..., {signal}) 的取消控制整个 Loop,不能通过计划的 catch 继续启动后续 Graph。单个 graphStep 的 signal 可用于阶段超时;计划可以捕获阶段错误并交出失败登记 Graph。整体信号和阶段信号会合并,阶段配置不能取消整体限制。

执行次数耗尽会终止 Loop,计划不能捕获预算错误并继续执行。终止时关闭生成器;同步 finally 可清理纯本地状态,其中交出的 Graph 不会执行。需要补偿或业务失败登记时,应在正常的错误分支中显式交出对应 Graph,并为它预留预算。Runtime.close 会等待已接收的 Loop 和正在执行的 Graph 结束,再关闭 Worker。

检查点与进程恢复 ​

Loop 不将生成器栈保存到数据库,也不自动重放外部副作用。应用用 MEMORY.GET/WRITE/UPDATE Graph 保存可验证的阶段结果。新进程重新启动同一计划,读取检查点并跳过已完成步骤;原有副作用核对、权限校验、Redis 恢复和幂等交付仍在计划中明确执行。

RAG 的 runRagLoop 与 12 类基础能力的 run*Loop 都从示例模块导出。runRag() 等便利函数只负责调用一次 runtime.loop()。检查点、模型、工具和交付 Graph 都由这个 Loop 调度。外部控制器的独立资料导入仍可以单次 runtime.run(),不会隐藏在 Agent 执行计划内。

RAG 的阶段组织 ​

mermaid
flowchart LR
    A[可信请求] --> L[runRagLoop]
    L --> G1[授权与 Memory Graphs]
    G1 --> L
    L --> G2[Context → 模型 Graph]
    G2 --> L
    L --> G3[资料接入与检索 Graphs]
    G3 --> L
    L --> G4[筛选、回答、依据检查 Graphs]
    G4 --> L
    L --> G5[保存与交付 Graphs]
    G5 --> R[带引用的结果]

计划根据检查点、澄清需求、证据及校验结果选择下一 Graph。回答修订最多一次;总 Graph 执行数另受 Loop 预算限制。业务数据库、第三方 SDK 和素材解析仍属于应用工具。

Ditto · @codesoul-co/ditto · Node.js 24+