项目目录

CALL-E / 产品

三条典型产品旅程与生命周期 Owner

交互式 Outbound、Published Goal 的 API 执行、Inbound Hotline 三条旅程共用同一条主干;分岔在「执行」之后——区别是谁负责往下推。

进行中

本文依据知识转移文档 docs/calle-agentic-knowledge-transfer.md(基线 2026-08-12)的第 3、4 章整理。三条旅程不是三种互斥的架构类型,而是同一条主干上的三种常见走法。开发者视角下更细的逐步操作流程(Simulation → Live Test → 真实呼叫测试 → API 集成 → 生产后改进)见 Goal 的生命周期 PRD

1. 共同主干

Text
理解目标 → 保存目标 → 准备方案 → 验证 → 授权 → 执行 → 留下证据 → 复盘与迭代

每一步的内容因路径而异:

阶段OutboundInbound
定义目标完成预约、跟进、催收或信息确认上线一条能持续处理某类来电的热线
补齐事实联系人、号码、时间、停止条件知识来源、处理范围、升级条件、禁止动作
准备方案生成话术和 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:帮我订餐厅,没人接就改时间再试

  1. 提出目标 —— 用户正常说话,MainAgent 接。没有副作用。
  2. 只问阻塞问题 —— 最多一两个:餐厅电话?预订人姓名?outbound-planner 判定 ready / 待补充 / 阻塞。问不清就停住,不猜。
  3. 确认,Goal 落库 —— 可读摘要 + 确认。这时只承诺「目标已保存」,不承诺已安排。
  4. 生成方案 —— OutboundGoalAgent 出话术和 RunSpec,明确告知「尚未测试、未发布」。缺关键事实就回来问,不编。
  5. 检查 / Simulation —— 「在 N 个场景演练过,结论是可以 / 不可以 / 不确定」。Runtime 编排,Judge 判单场景。证据不全一律判不确定,不放行。
  6. 批准真实拨打 —— 用户看到确切号码和确切话术版本。批准之后即使进程退出,也会恢复同一次动作,不会重复拨打。
  7. 执行并落库 —— Voice Agent 打,Runtime 存。结果先落库再通知,通知失败可以重投。
  8. 汇报并决定下一步 —— 「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:上线一条能回答物流和退款问题的客服热线

用户原话大概是:「帮我上线一个客服热线,能回答物流和退款;不确定或涉及退款审批时转人工。」

  1. 描述服务目标 —— MainAgent 接。
  2. 问不可推导的事 —— 材料在哪?哪些不能回答?什么情况转人工?用什么语言?能从材料或配置推导出来的,不许反问用户。
  3. 确认,创建 Inbound Goal —— 「目标已保存,开始整理资料」。这时既没建 bot,也没绑号码。
  4. 整理知识 + 生成策略 —— InboundGoalAgent 从上传材料里整理出知识范围。知识只来自来源材料;改动生成新版本,旧版本仍可追溯。
  5. 用 persona 演练 —— 模拟退款、超时、无理由退货等场景。演练针对确切的候选版本,换版本要重演。
  6. 自动修复并重演 —— 「发现 2 个回答不准,已修正并重新演练」。有次数上限;用尽了就报告修了什么、还剩什么风险、建议怎么办,不把实现任务甩给用户。
  7. 报告 + 批准绑定 —— onboarding 报告说清楚理解了什么、演练了什么、还有什么局限、激活会产生什么效果。报告只是决策上下文,不是授权凭证;绑定前会重新校验版本和证据是否仍然一致。
  8. 热线上线 —— 设置发布指针,Goal 变 active。版本过期、审批被拒或 provider 失败都不会移动指针。
  9. 持续接听 —— Voice Agent 负责。单通失败不影响热线状态,也不结束 Goal。
  10. 周期复盘 —— 「本周 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

这大概是整个架构里最容易被质疑的一处复杂度。原因是两件事在三个维度上要求正好相反:

维度MainAgentGoalAgent
Context当前用户会话当前 Goal 的决策与事实
触发用户发送消息Runtime 事件、审批、执行结果、定时任务
生命周期会话级Goal 级,跨会话、跨天
输出面向用户的表达方案、决策、delivery

塞进同一个 Agent,这几行会互相打架。拆开换来的是:GoalAgent 有独立 context、Runtime 可以主动唤醒它、任意会话都能接着看同一个目标(会话由 goal_id 派生,跟聊天窗口无关)。

代价是两者不共享完整上下文,用户看到什么完全取决于结构化 delivery 和那条摘要的质量。所以投递摘要的质量是产品要负责的内容——摘要写差了,用户就只看到「发现了一些问题」这种空话。

另外提醒一句:执行状态不是 Goal 状态。一次通话失败不会让目标失败,成功也不会让目标完成。目标什么时候结束,由用户和 GoalAgent 决定。

接下来看分层定位与迭代 Playbook:一个 bad case 该改哪一层,以及一次完整迭代怎么走。

源码审计版本 calle-agentic-knowledge-transfer@2026-08-12