AB
AiBoss站
教程

Salesforce Agent Script 教程:用代码约束 Agentforce 代理行为

教程

Salesforce Agent Script 教程:用代码约束 Agentforce 代理行为

Agent Script 让 Agentforce 代理中「必须每次一致」的部分由代码保证,把灵活发挥留给 LLM。本教程从变量、路由到子代理,逐步搭建一个面向销售人员的员工代理,并说明推理前后钩子、模型选择、动作入参、记忆等关键设置。

用 Agentforce 搭建代理时,常见的一个困扰是:提示词里明明写了「必须先确认客户再回答」,实际运行时这一步却被跳过,模型直接给出了答案。大语言模型很聪明,但也带点随性,写了「必须」不等于每次都会照做。一旦要放进真实业务流程,总有一些环节需要每次都保持同样的行为。Agent Script 就是为此准备的:把可以灵活发挥的部分交给 LLM,把必须稳定的部分用代码写死。它把代理定义收敛到一个文件里,用代码描述状态、流程与钩子。本教程面向刚开始接触 Agentforce 的管理员与开发者,从基础语法讲到用三个子代理搭出一个面向销售人员的员工代理。

准备工作

在动手之前,先确认下面这些前提。

  • 一个可用的 Salesforce 组织,并且已经开通 Agentforce 相关能力。Agent Script 在 Dreamforce 2025 上公布,2025 年 11 月进入公开测试阶段,具体可用范围、版本与配额请以官网当前信息为准。
  • 能够打开 Agentforce Builder(从 Agentforce 工作室应用进入的代理创建工具)。脚本视图是编写与查看 Agent Script 的主要入口,边看模板边写是最快的方式。
  • 具备创建 Flow(流)的权限。本教程中的取数逻辑放在自动启动流里,由代理动作调用。
  • 明确要做的代理类型。这里做的是供内部员工使用的「Agentforce Employee Agent」,面向销售人员的日常助手,与面向客户的「Agentforce Service Agent」不是同一个东西。
  • 想清楚哪些环节必须稳定、哪些可以交给模型。变量、路由器、子代理按这个顺序梳理,写起来会顺很多。

Agent Script 能做什么

概括起来是三件事:让变量持有状态,不依赖 LLM 的记忆;用代码决定子代理之间的跳转;在推理前后插入处理逻辑。代码由若干块组成,各块分工如下。

块作用
system代理整体指令、欢迎语与错误提示
config代理的元信息
variables变量声明
start_agent入口,判断把请求交给哪个子代理
subagent单个功能,持有动作与推理指令

变量在正文里写成 {!@variables.变量名},在表达式里写成 @variables.变量名。赋值用 set @variables.变量名 = 值,跳转到别的子代理用 @utils.transition to @subagent.子代理名。写给 LLM 的指令用竖线 | 起头,不必每行都加,一整段只在开头写一次,后续行靠缩进延续。

instructions : -> | 你是员工代理。
请礼貌回应用户的请求。
不确定的事情不要猜测,先确认。

中间如果插入 if 或 run,就再起一个新的 |,按上下文分段。

操作步骤

第一步:先声明变量

变量要提前全部列出来。每个变量都写清楚 description,这是 LLM 判断「什么时候该用这个变量」的依据。用于接收用户当前打开页面信息的变量,需要加 visibility: "External";只做临时承载的变量,初始值设为空字符串。

variables :
  currentObjectApiName : mutable string
    description : "用户当前打开记录的 Object API 名(Account、Opportunity 等)。用户谈论其他对象时不要使用。"
    visibility : "External"
  currentRecordId : mutable string
    description : "用户当前打开记录的 Id。仅在出现「这个」「当前的」这类指代时使用,拿不准就不要用。"
    visibility : "External"
  current_subagent : mutable string = ""
    description : "上一轮对话由哪个子代理处理。各子代理在 before_reasoning 中写入,路由器在第二次及以后的分配中作为提示。"
  target_account_id : mutable string = ""
    description : "摘要对象的客户 Id。打开客户页面时取页面 Id,否则取检索确定的 Id。"
  opportunity_text : mutable string = ""
    description : "GetAccountSummaryData 取得的、与客户关联的商机文本。"
  todo_text : mutable string = ""
    description : "GetAccountSummaryData 取得的、与客户关联的待办文本。"
  event_text : mutable string = ""
    description : "GetAccountSummaryData 取得的、与客户关联的活动文本。"
  summary_account_id : mutable string = ""
    description : "已经取过摘要数据(opportunity_text 等)的客户 Id,用于判断是否已取过。"

其中 current_subagent 的作用容易被低估。用户看完摘要后接着说「再详细一点」「只看商机」,如果路由器每次都只凭这一句话重新分配,很可能被分到别的子代理去,越短的追加提问越容易出问题。做法是:各子代理在 before_reasoning 里把自己的名字写进 current_subagent;路由器发现它非空,就告诉 LLM「上一轮是某某在处理」;如果用户是在追问,就交给同一个子代理,明显是另一件事时则不依赖这个变量,重新判断;请求落入兜底子代理(意图模糊或超出范围)时,把变量清空。

第二步:写路由器

路由器写在 start_agent 里,把跳转动作排开。真正影响分配结果的是每个动作的 description,把「什么请求该用它」连同示例说法一起写进去,分配会稳定很多。

start_agent agent_router :
  label : "Agent Router"
  description : "欢迎用户,并根据请求决定合适的子代理"
  reasoning :
    instructions : ->
      | 你是员工代理的路由器。
        请根据用户请求,选择最合适的一个动作。
      if @variables.current_subagent != "" :
        | 上一轮对话由子代理「{!@variables.current_subagent}」处理。
          如果用户这句话是它的延续(追加提问、缩小范围、修改要求等),
          不要重新判断,直接选择该子代理对应的 go_to_ 动作。
          如果明显是另一件事,就不要依赖这条信息,按请求内容重新决定去向。
      if @variables.currentObjectApiName == "Account" :
        | 用户正打开客户页面。
          「这个客户」「摘要」这类请求请选择 {!@actions.go_to_Account_Summary}。
  actions :
    go_to_Account_Summary : @ utils.transition to @subagent.Account_Summary
      description : "需要客户摘要(商机、待办、活动的汇总)时使用。例如「把某某公司的情况汇总一下」。"
    go_to_Visit_Prep : @ utils.transition to @subagent.Visit_Prep
      description : "需要为下次拜访做准备(生成议程、要确认的问题)时使用。例如「我想准备下次拜访」。"
    go_to_UserMemoryManagement : @ utils.transition to @subagent.UserMemoryManagement
      description : "需要查看、修改、删除记忆内容,或开关记忆时使用。例如「给我看看你记得我什么」。"
    go_to_ambiguous_question : @ utils.transition to @subagent.ambiguous_question
      description : "请求意图模糊、无法判断哪个子代理合适时使用。"
    go_to_off_topic : @ utils.transition to @subagent.off_topic
      description : "与销售业务无关的闲聊或一般性知识提问时使用。"

「超出范围」和「意图模糊」两个兜底子代理不要省。没有兜底,请求会被硬塞给某个子代理。

第三步:做取数用的流

客户摘要子代理的思路是:取数交给流动作确保执行,取回的结果放进变量,只让 LLM 负责排版和总结。先建流,再写子代理。

流的规格:类型为自动启动流(无触发器),API 参照名为 Get_Account_Summary_Data,输入 accountId(文本),输出 opportunityText、todoText、eventText(均为文本)。

  1. 打开「设置」,在快速搜索里找「流」并进入。
  2. 选择「新建流」→「从头开始」→「下一步」,选中「自动启动流(无触发器)」后创建。
  3. 在「新建资源」里建四个变量,数据类型都是文本。accountId 勾选「可供输入使用」,其余三个勾选「可供输出使用」。只有勾了这两个选项的变量才会成为动作的输入与输出。
  4. 添加「获取记录」元素,对象选「商机」,条件为「客户 ID」等于 {!accountId} 且「已结束」等于 {!$GlobalConstant.False},按「预计结束日期」升序排列。保存的记录数选「所有记录」,保存字段收敛到商机名称、阶段、金额、预计结束日期几项。
  5. 添加「循环」元素,逐条处理取回的集合。
  6. 在「新建资源」里建「文本模板」,拼出一行文字,例如:・{!Loop_Opportunities.Name}({!Loop_Opportunities.StageName}/{!Loop_Opportunities.Amount}/预计结束日期 {!Loop_Opportunities.CloseDate})。
  7. 在循环里用「分配」把模板以「添加」运算符累加到 opportunityText。
  8. 待办与活动按同样的流程处理。待办:对象为「待办」,条件为「相关对象 ID」等于 {!accountId} 且「已完成」等于 {!$GlobalConstant.False},按到期日升序,把主题、到期日、状态累加到 todoText。活动:对象为「活动」,条件为「相关对象 ID」等于 {!accountId} 且「开始日期时间」在最近三个月内,按开始时间降序,把主题、开始时间累加到 eventText。最近三个月可以用「新建资源」里的「公式」(数据类型为日期/时间)写成 NOW() - 90。
  9. 处理零条记录的情况。零条会让文本为空,LLM 就分不清「没有数据」和「取数失败」。在每个「获取记录」后面加「判定」,零条时用「分配」写入「无对应记录」。
  10. 保存,填好显示标签与 API 参照名,在「描述」里写清作为动作使用时的说明,然后启用。

流里还可以为取数失败留一条痕迹,例如把 eventText 赋值为 获取失败({!$Flow.FaultMessage}),方便排查。

第四步:写客户摘要子代理

取数用流动作确保执行,结果放进变量,LLM 只做排版与总结。因为输出格式固定,这里指定一个轻量快速的模型。

subagent Account_Summary :
  model_config :
    model : "model://sfdc_ai__DefaultBedrockAnthropicClaude45Haiku"
  before_reasoning :
    set @variables.current_subagent = "Account_Summary"
    if @variables.currentObjectApiName == "Account" and @variables.target_account_id == "" :
      set @variables.target_account_id = @variables.currentRecordId
  reasoning :
    instructions : ->
      if @variables.target_account_id == "" :
        | 还没有确定客户。
          请向用户确认客户名称。
      else :
        | 请为客户 ID {!@variables.target_account_id} 生成摘要。
  actions :
    IdentifyRecordByName : @ actions.IdentifyRecordByName
      with recordName=...
    GetAccountSummaryData : @ actions.GetAccountSummaryData
      with accountId = @variables.target_account_id
    CreateTodo : @ actions.CreateTodo
      available when @variables.summary_confirmed == True

动作入参有两种写法:with recordName=... 交给 LLM 从对话里填值,适合客户名称这类需要从会话中拾取的信息;with accountId = @variables.target_account_id 由代码固定,LLM 碰不到,适合 Id 这类错了就麻烦的值。整体流程是:LLM 从对话里拾取客户名称,用 IdentifyRecordByName 检索确定 Id,再由 set_target_account 把 Id 存进变量,之后一律由代码传这个变量,不让 LLM 写 Id。如果用户正打开客户页面,before_reasoning 已经把页面 Id 放进 target_account_id,不必从对话里猜。

还要注意一点:用变量固定的值如果为空就去调动作,会出问题。用 if 先准备一条「为空时先补齐」的分支。

第五步:推理前后的钩子

子代理可以放 before_reasoning 与 after_reasoning 两个块,分别在推理前和推理后每次执行,里面可以更新变量、执行动作、跳转到别的子代理。「每次都必须记录」「这个变量必须更新」这类处理,不要交给 LLM 判断,放在这里更稳妥。

有几个容易踩的点:这两个块里的跳转写 transition to,不是 @utils.transition to;块里不能写 | 开头的指令文本;推理途中跳到别的子代理时,原来的 after_reasoning 不会执行。

第六步:按子代理选模型

用 model_config 可以按代理指定模型,也可以按子代理指定,子代理的设置优先。只做固定格式输出的子代理配轻量快速的模型,需要构思议程的子代理配推理能力更强的模型。这直接关系到成本与响应速度,早点定下来后面省事。

第七步:开启记忆

记忆会从对话中自动抽取偏好与事实,按用户保存,并在后续回答中体现,适合每次都要重复交代前提的场景。设置写在 context 块里。

context :
  memory :
    enabled : True
  user_profile :
    enabled : True

user_profile 用于使用姓名、邮箱、区域设置、时区等资料信息,它与记忆是两套功能,资料信息不会被当作记忆保存。记忆按代理、按用户保存,每个用户最多 50 条,超出后从最旧的开始清除;它与会话日志是两回事;在 Lightning Experience 中使用员工代理时,需要用户单独选择启用;保存的数据留在 Salesforce 内,不会与 LLM 共享。如果希望用户能在对话里说「记住」「给我看看你记得什么」「关掉记忆」,把资产库里的标准子代理「User Memory Management」加进来即可。

一个完整示例

把上面的部分串起来,就是一个可运行的最小结构:一个路由器加三个子代理。

system :
  instructions : ->
    | 你是面向销售人员的员工代理。
      请礼貌、简洁地回应用户。

config :
  label : "Employee Agent"

context :
  memory :
    enabled : True
  user_profile :
    enabled : True

variables :
  current_subagent : mutable string = ""
  target_account_id : mutable string = ""
  opportunity_text : mutable string = ""
  todo_text : mutable string = ""
  event_text : mutable string = ""

start_agent agent_router :
  label : "Agent Router"
  description : "欢迎用户,并根据请求决定合适的子代理"
  reasoning :
    instructions : ->
      | 请根据用户请求选择最合适的一个动作。
      if @variables.current_subagent != "" :
        | 上一轮由「{!@variables.current_subagent}」处理。
          如果这句话是它的延续,选择同一个子代理对应的动作。
  actions :
    go_to_Account_Summary : @ utils.transition to @subagent.Account_Summary
      description : "需要客户摘要时使用。"
    go_to_Visit_Prep : @ utils.transition to @subagent.Visit_Prep
      description : "需要准备下次拜访时使用。"
    go_to_UserMemoryManagement : @ utils.transition to @subagent.UserMemoryManagement
      description : "需要查看或修改记忆时使用。"
    go_to_ambiguous_question : @ utils.transition to @subagent.ambiguous_question
      description : "意图模糊时使用。"
    go_to_off_topic : @ utils.transition to @subagent.off_topic
      description : "与销售无关的提问时使用。"

subagent Account_Summary :
  model_config :
    model : "model://sfdc_ai__DefaultBedrockAnthropicClaude45Haiku"
  before_reasoning :
    set @variables.current_subagent = "Account_Summary"
  reasoning :
    instructions : ->
      if @variables.target_account_id == "" :
        | 还没有确定客户,请向用户确认客户名称。
      else :
        | 请为 {!@variables.target_account_id} 生成摘要。
  actions :
    IdentifyRecordByName : @ actions.IdentifyRecordByName
      with recordName=...
    GetAccountSummaryData : @ actions.GetAccountSummaryData
      with accountId = @variables.target_account_id

subagent Visit_Prep :
  before_reasoning :
    set @variables.current_subagent = "Visit_Prep"
  reasoning :
    instructions : ->
      | 请基于已取得的商机、待办、活动文本,整理下次拜访的议程和要向对方确认的问题。

subagent UserMemoryManagement : @ subagent.UserMemoryManagement

subagent ambiguous_question :
  reasoning :
    instructions : ->
      | 请求不够明确,请向用户确认想做什么。
  before_reasoning :
    set @variables.current_subagent = ""

subagent off_topic :
  reasoning :
    instructions : ->
      | 这不在销售业务范围内,请礼貌说明并引导回正题。
  before_reasoning :
    set @variables.current_subagent = ""

运行时的顺序是:用户提问 → 路由器按动作描述分配 → 子代理在 before_reasoning 记录自己的名字并补齐变量 → 推理前按 if 分支给出不同指令 → 需要取数时用 run 执行动作并把结果写进变量 → 推理后按需更新状态。取数结果落在变量里,LLM 只负责排版与总结,输出形态因此稳定。

注意事项

  • Agent Script 目前处于公开测试阶段,功能范围、可用地区与配额可能变化,落地前请以官网当前信息为准。
  • 提示词里的「必须」不能保证每次都执行。需要稳定复现的环节,用 if、run、available when 写成代码。
  • before_reasoning 与 after_reasoning 中的跳转写 transition to,不是 @utils.transition to;这两个块里不能写 | 指令文本。
  • 推理途中跳转到别的子代理时,原来的 after_reasoning 不会执行,依赖它收尾的逻辑要重新安排。
  • 用变量固定的入参如果为空就调用动作,会得不到预期结果,先写「为空时补齐」的分支。
  • 取数返回零条时,空文本会让 LLM 无法区分「没有数据」和「取数失败」,要显式写入「无对应记录」。
  • 记忆按用户最多保存 50 条,超出后从最旧的开始清除;它与会话日志相互独立,数据留在 Salesforce 内,不与 LLM 共享。
  • 在 Lightning Experience 中使用员工代理时,记忆需要用户单独选择启用。
  • 路由器的分配质量取决于各动作 description 的写法,把使用场景和示例说法写清楚,并保留「意图模糊」与「超出范围」两个兜底子代理。