本文依据知识转移文档 docs/calle-agentic-knowledge-transfer.md(基线 2026-08-12)的第 3、4 章整理。三条旅程不是三种互斥的架构类型,而是同一条主干上的三种常见走法。开发者视角下更细的逐步操作流程(Simulation → Live Test → 真实呼叫测试 → API 集成 → 生产后改进)见 Goal 的生命周期 PRD。
1. 共同主干
理解目标 → 保存目标 → 准备方案 → 验证 → 授权 → 执行 → 留下证据 → 复盘与迭代每一步的内容因路径而异:
| 阶段 | Outbound | Inbound |
|---|---|---|
| 定义目标 | 完成预约、跟进、催收或信息确认 | 上线一条能持续处理某类来电的热线 |
| 补齐事实 | 联系人、号码、时间、停止条件 | 知识来源、处理范围、升级条件、禁止动作 |
| 准备方案 | 生成话术和 RunSpec | 把来源知识编译成 inbound strategy |
| 验证 | 用场景检查话术 | 用 persona 检查回答与升级 |
| 授权 | 批准确切的拨打动作 | 批准绑定确切热线和版本 |
| 执行 | 打一通 | 持续接每一通 |
| 迭代 | 重试、改时间、改策略或结束 | 更新知识,重新验证并发布 |
2. 三种「批准」要分清
可复用 Goal 走 Outbound 这一列,到「授权」才分家。三种批准语义不同:
- 对话里的确认 —— 用户在 chat 中同意某个方案或某次动作。是产品语义,不是权限机制。
- protected-tool approval —— SDK 在 handler 执行前中断,审批对象是确切的 tool name、arguments 和 call id。今天只有
submit_voice_run(真实拨打)和bind_hotline(热线绑定)走这条。 - 发布契约 ——
publish_goal_run_spec确定哪个 RunSpec 版本可被执行。它不是 protected tool,后续每次 Run 由调用方依据该契约触发。
所以:交互式 Outbound 在每次真实拨打前过 protected-tool approval;Inbound 在绑定热线前过 protected-tool approval;Published Goal 靠发布契约锁定版本,单次 Run 由调用方发起,也不唤醒 GoalAgent。
3. Outbound:帮我订餐厅,没人接就改时间再试
- 提出目标 —— 用户正常说话,MainAgent 接。没有副作用。
- 只问阻塞问题 —— 最多一两个:餐厅电话?预订人姓名?
outbound-planner判定 ready / 待补充 / 阻塞。问不清就停住,不猜。 - 确认,Goal 落库 —— 可读摘要 + 确认。这时只承诺「目标已保存」,不承诺已安排。
- 生成方案 —— OutboundGoalAgent 出话术和 RunSpec,明确告知「尚未测试、未发布」。缺关键事实就回来问,不编。
- 检查 / Simulation —— 「在 N 个场景演练过,结论是可以 / 不可以 / 不确定」。Runtime 编排,Judge 判单场景。证据不全一律判不确定,不放行。
- 批准真实拨打 —— 用户看到确切号码和确切话术版本。批准之后即使进程退出,也会恢复同一次动作,不会重复拨打。
- 执行并落库 —— Voice Agent 打,Runtime 存。结果先落库再通知,通知失败可以重投。
- 汇报并决定下一步 —— 「7 点满座,8 点有位,改到 8 点再打一次吗?」模型连续失败不打扰用户,最后会给一条能操作的说明。
第 8 步之后的自动改时间续拨目前还不是生产能力。今天是:GoalAgent 复盘 → 给建议 → 用户一句话确认 → 复用同一个 Goal 再打(不用重新配置)。「一次确认覆盖多个拨打槽位、由 Runtime 守住时间和次数」还在草稿里(goal-call-strategy spec)。
4. 可复用的 Goal(Draft):发布之后交给 API
订餐厅那条是一次性的,办完就结束。另一种形态是把 Goal 做成可复用的模板:前半段一样(intake、生成候选、Simulation 验证),验证过之后发布出去,让外部系统按 goal_id 反复调用。
- 发布 ——
publish_goal_run_spec把验证过的 RunSpec 版本设成已发布指针。在这之前它一直是「未测试、未发布的候选」。 - 外部触发 ——
POST /v1/goals/{goal_id}/runs,带上这次的变量(打给谁、说什么),CALL-E 用已发布那个版本执行一次;结果从GET .../runs/{goal_run_id}取。契约在sdks/openapi/calle.openapi.yaml,spec 仍是 Draft。
这条路上 GoalAgent 不参与每一通。已发布 Goal 的 Run 结束后不创建 Goal 事件、不唤醒 Agent、不产生 delivery,结果直接落成结构化 projection 等调用方来取——一天被调一千次,不可能每通都叫个 Agent 起来想一想。
所以「谁在推进这个目标」两条路上答案不同:交互式是 GoalAgent,已发布是调用方自己。同一个 bad case 出在哪条路上,该改的东西完全不一样。
5. Inbound:上线一条能回答物流和退款问题的客服热线
用户原话大概是:「帮我上线一个客服热线,能回答物流和退款;不确定或涉及退款审批时转人工。」
- 描述服务目标 —— MainAgent 接。
- 问不可推导的事 —— 材料在哪?哪些不能回答?什么情况转人工?用什么语言?能从材料或配置推导出来的,不许反问用户。
- 确认,创建 Inbound Goal —— 「目标已保存,开始整理资料」。这时既没建 bot,也没绑号码。
- 整理知识 + 生成策略 —— InboundGoalAgent 从上传材料里整理出知识范围。知识只来自来源材料;改动生成新版本,旧版本仍可追溯。
- 用 persona 演练 —— 模拟退款、超时、无理由退货等场景。演练针对确切的候选版本,换版本要重演。
- 自动修复并重演 —— 「发现 2 个回答不准,已修正并重新演练」。有次数上限;用尽了就报告修了什么、还剩什么风险、建议怎么办,不把实现任务甩给用户。
- 报告 + 批准绑定 —— onboarding 报告说清楚理解了什么、演练了什么、还有什么局限、激活会产生什么效果。报告只是决策上下文,不是授权凭证;绑定前会重新校验版本和证据是否仍然一致。
- 热线上线 —— 设置发布指针,Goal 变 active。版本过期、审批被拒或 provider 失败都不会移动指针。
- 持续接听 —— Voice Agent 负责。单通失败不影响热线状态,也不结束 Goal。
- 周期复盘 —— 「本周 8 通没答上来,集中在退款时效」+ 改进建议。产出的是草稿候选,不会自动上线。
Inbound 有两点跟 Outbound 不一样。一是有界自动修复:Agent 自己能发现并修掉的问题就自己修、自己重演,只有用户才知道的事实、或者要改变已确认边界时才回来问。二是上线不是终点:热线 active 之后进入复盘循环,识别知识缺口,产出待验证的改进候选。
还有一条要反复强调:InboundGoalAgent 不参与任何一通实时对话。它拥有的是热线的建设、验证和改进生命周期,每一通来电由 Voice Agent 独立完成。这两件事在时间尺度上差几个数量级,混在一起就没法保证实时性。
最后,周期复盘默认关闭。Goal 首次上线后系统会明确问一次,说明这会增加成本,给「不开启 / 每周 / 每日」三个选择——不默认替用户打开会花钱的后台分析。
6. 产品概念与组件
Chat history 帮助模型推理,但 Goal、RunSpec、Run 和 Evidence 才是产品事实。组件是为了维护这些对象存在的,不是反过来。
| 对象 | 回答的问题 |
|---|---|
| Goal | 「我要完成什么」——跨会话、跨执行持续存在的目标 |
| RunSpec | 「系统准备怎么做」——不可变、可追溯的执行版本(话术 + 契约 + 配置),包含但不限于 Voice Agent Instruction |
| Run | 「这一次实际执行」——交互式由受保护 Tool 创建,Published Goal 由已授权的外部调用契约创建;进入真实执行后不能靠改写历史撤销 |
| Evidence / Artifact | 「为什么得到这个结论」——通话记录引用、结果 receipt、评测结果、报告;只追加,不改写 |
| Delivery | 「系统现在要告诉或问我什么」——GoalAgent 声明,Runtime 投递,MainAgent 表达 |
四个生命周期 Owner:
- MainAgent 拥有会话 —— 理解目标、只问必要问题、拿确认、表达结果。不拥有 Goal 策略状态,不提交执行,不代理审批。
- GoalAgent 拥有 Goal —— 出方案、准备执行、验证、处理结果、周期改进。不能绕过审批。
- Runtime 拥有确定性骨架 —— 状态、调度、恢复、幂等、权限、生命周期。不做开放式语义判断。
- Voice Agent 拥有单通执行 —— 一次低延迟实时对话。不拥有 Goal、发布或跨通报告。
两个辅助角色(不拥有任何生命周期):Judge —— 隔离上下文里的语义判断,无工具、无 session、不改状态;Materializer —— 一次性结构化转换,把终态结果落成 receipt。
有一条边界会反复用到:MainAgent 和 GoalAgent 只能经 Runtime 的 state / event / delivery 协作。
7. 为什么要拆成 MainAgent 和 GoalAgent
这大概是整个架构里最容易被质疑的一处复杂度。原因是两件事在三个维度上要求正好相反:
| 维度 | MainAgent | GoalAgent |
|---|---|---|
| Context | 当前用户会话 | 当前 Goal 的决策与事实 |
| 触发 | 用户发送消息 | Runtime 事件、审批、执行结果、定时任务 |
| 生命周期 | 会话级 | Goal 级,跨会话、跨天 |
| 输出 | 面向用户的表达 | 方案、决策、delivery |
塞进同一个 Agent,这几行会互相打架。拆开换来的是:GoalAgent 有独立 context、Runtime 可以主动唤醒它、任意会话都能接着看同一个目标(会话由 goal_id 派生,跟聊天窗口无关)。
代价是两者不共享完整上下文,用户看到什么完全取决于结构化 delivery 和那条摘要的质量。所以投递摘要的质量是产品要负责的内容——摘要写差了,用户就只看到「发现了一些问题」这种空话。
另外提醒一句:执行状态不是 Goal 状态。一次通话失败不会让目标失败,成功也不会让目标完成。目标什么时候结束,由用户和 GoalAgent 决定。
接下来看分层定位与迭代 Playbook:一个 bad case 该改哪一层,以及一次完整迭代怎么走。