Parlant Canned Responses 实战:四阶段响应合成、三种 Composition Mode 与防幻觉模板设计
2026/9/13 22:44:06 网站建设 项目流程

Parlant Canned Responses 实战:四阶段响应合成、三种 Composition Mode 与防幻觉模板设计

【免费下载链接】parlantBuild reliable customer-facing AI agents with Parlant: an interaction control harness optimized for controlled, consistent, and predictable LLM interactions.项目地址: https://gitcode.com/GitHub_Trending/pa/parlant

Canned responses(预置响应)是 Parlant 对 Agent 输出施加"精确控制"的核心机制:它把 Agent 的回复从"逐 token 自由生成"收敛为"从预审批模板集合中检索、渲染、选择",从而在保持对话流畅性的同时,彻底消除措辞漂移与细微幻觉。本文以 canned-responses.md 为主线,结合 CannedResponseGenerator、CannedResponseStore 等源码实现,完整讲解四阶段工作机制、Fluid/Composited/Strict 三种 composition mode 的取舍、std./generative./工具字段三类模板字段的用法、signals 检索优化,以及 no-match 响应的两种自定义方式,帮助你为生产环境中的高风险对话设计可控、可审计的响应体系。

一、什么是 Canned Responses:来自呼叫中心的概念

Canned responses 的概念源自真实呼叫中心:坐席从一组预先批准的话术中选择回复,以保证沟通的一致性、准确性与品牌口径统一。在 Parlant 中,这组预定义、预批准的响应集合把 Agent 的输出约束在固定风格与既定事实范围内,完全消除即使是细微的、非预期的或幻觉性输出的风险

文档中用了一个很贴切的比喻:canned responses 就像一手扑克牌——你给 Agent 提供一组可选的"牌",它基于对话上下文从中选出最合适的一张。

一个直观示例

不用 canned responses 时,LLM 逐 token 生成:

客户:Do you have it in stock?

Agent:Yes, we've got this item in stock! Let me know if you need any help finding it.

启用 canned responses 后,引擎先有一个内部草稿(draft message),然后从候选模板中挑选:

# Draft message: "Yes, we've got this item in stock! Let me know if you need any help finding it." # # Available templates: # - ... # - "Hey, {{std.customer.name}}! What help do you need today?" # - ... # - "No, sorry, we've just sold the last ones. Would you like to see something similar?" # - "Yep, we have it. Should I add it to your cart?" # - ...

客户:Do you have it in stock?

Agent:Yep, we have it. Should I add it to your cart?

最终发送的是被选中的模板("Yep, we have it. Should I add it to your cart?"),而不是逐 token 生成的草稿——草稿只是检索与比对的基准。

二、四阶段响应生成机制

Canned responses 在引擎内部是一个四阶段流水线:

  1. 起草(Draft):Agent 基于当前的情境感知(交互历史、guidelines、工具结果等)先起草一条 fluid 消息;
  2. 检索(Retrieve):引擎根据草稿消息,检索最相关的 canned response 模板作为候选;
  3. 渲染(Render):引擎渲染候选模板,在适用处用工具提供的字段值做替换;
  4. 选择(Select):基于草稿消息,Agent 从候选中选出最贴合的一条 canned response 输出。

从源码结构看,这四个阶段在 CannedResponseGenerator._generate_response 中逐一对应,且每个阶段都有独立的计时直方图(canrep.draftcanrep.retrievalcanrep.rendercanrep.selection),便于在生产环境做性能观测:

  • 阶段 1调用self._canrep_draft_generator生成结构化草稿(输出 schema 为CannedResponseDraftSchema,其中response_body即最终草稿文本);
  • 阶段 2调用 CannedResponseStore.filter_relevant_canned_responses 做向量相似度检索,最多取max_count=30条,并按candidate_similarity_threshold = 0.4过滤(见 canned_response_generator.py#L553 与 #L2267-L2278);
  • 阶段 3_render_responses完成 Jinja2 渲染,任一字段缺失即判定该模板渲染失败并被剔除(#L2449-L2495);
  • 阶段 4由选择器 LLM 输出chosen_template_idmatch_quality(取值low/partial/high),随后按 composition mode 决定最终消息(详见下一节)。

三、Composition Modes:三种自由度级别

Parlant Agent 的响应采用一种composition mode,不同模式对输出的约束程度、以及对 canned responses 的使用方式各不相同:

模式说明适用场景
FluidAgent 优先从 canned responses 中选择(若能找到足够好的匹配),否则回退到默认的消息生成。(A) 整体保持流畅,只在特定场景/响应处施加控制;(B) 原型设计阶段,边构建 fluid 推荐边补充更多话术。
CompositedAgent 只用 canned response 候选来改写(recompose)生成的草稿消息,使其模仿检索到的候选的风格。对语气(tone of voice)敏感的品牌场景。
StrictAgent 只能输出预置响应;若找不到匹配项,则发送一条可自定义的 no-match 消息。不允许出现任何细微或偶发幻觉的高风险场景。

提示:如果你面对高风险用例、对部署 GenAI Agent 心存顾虑,建议从 strict mode 起步。Parlant 足够灵活,等你准备好时可以平滑切换到更 fluid 的模式——切换 composition mode 时,你的对话模型其余部分(guidelines、journeys、tools 等)依然完整保留并生效。

设置 Agent 的 Composition Mode

创建 Agent 时传入composition_mode即可:

await server.create_agent( name="My Agent", description="An agent that uses canned responses", composition_mode=p.CompositionMode.STRICT, # or FLUID or COMPOSITED )

SDK 侧的默认值是CompositionMode.FLUID(见 sdk.py#L5125)。在核心层,CompositionMode 枚举 定义了FLUIDCANNED_FLUIDCANNED_COMPOSITEDCANNED_STRICT等取值,SDK 面向用户的STRICT/COMPOSITED对应内部的CANNED_*成员。

从源码结构看,各模式的运行时行为差异集中在 canned_response_generator.py 中:

  • Fluid:若根本没有可用 canned responses,或检索后没有任何相关候选,草稿消息直接发送(direct_draft_output_mode,#L2172-L2174);选择结果为lowpartial时同样回退为发送草稿(#L2354-L2403);
  • Composited:不走模板选择,而是调用_recompose,让 LLM 以渲染后的候选为"风格参考"改写草稿,且 prompt 中明确要求保留草稿的语义、只复制参考消息的语气与措辞(#L2317-L2334 与 #L2497-L2560);
  • Strictpartial质量的匹配也会被采用(#L2405 起);无匹配或模板 ID 非法时,走 no-match 流程(见第七节)。此外 strict 模式下还有一个细节:发送第一条消息后,引擎还会执行"follow-up 选择",判断草稿中是否有尚未被第一张模板覆盖的剩余内容,若有则再从候选里补发第二张模板(generate_follow_up_response,#L2750-L2825),这保证 strict 模式下草稿的信息量尽可能被预置话术完整表达。

值得注意:guidelines 也可以携带 composition mode,运行时由 _resolve_composition_mode 按"最严格者优先"(STRICT > COMPOSITED > FLUID)在 Agent 级模式与命中的 guideline 级模式之间解析出最终生效模式——这意味着你可以在保持 Agent 全局 fluid 的同时,对特定 guideline 触发的高风险话题局部收紧为 strict。

四、创建 Canned Responses

Agent 级创建

最基本的用法:

await agent.create_canned_response(template=TEXT)

SDK 侧 Agent.create_canned_response 接受templatetagssignalsmetadatafield_dependencies五组参数,底层调用CannedResponseStore.create_canned_response落库。创建时模板会先经过 Jinja2 语法校验(CannedResponseVectorStore._validate_template),非法模板会直接抛出ValueError

Journey-Scoped Responses(旅程作用域)

你也可以创建旅程作用域的响应:只有当某个 journey 处于激活状态时,这些响应才可被选用。把响应限定到 journey 能收窄候选集合,从而提高选中目标话术的概率。做法是把create_canned_response调用在具体 journey 实例上:

await journey.create_canned_response(template=TEXT)

从源码看,Journey 级实现 与普通创建唯一的区别是自动附加了一个 journey 归属 tag(_Tag.for_journey_id(self.id)),引擎在检索候选时通过 tag 过滤实现作用域隔离;而 Agent 级创建则附加 agent 归属 tag(#L3606-L3627)。

Preamble Responses(前导响应)

Parlant 利用"感知性能(perceived performance)"原则增强对话体验:在 Agent 生成完整、准确的回复之前,先发送一条简短的preamble response(如 "Got it."、"Understood."、"Let me look into that")来确认已收到客户输入。这些前导语默认由 Agent 根据上下文自动生成,你也可以创建自定义的 canned preamble 供 Agent 从中挑选。创建方式为加上preamble()tag:

await agent.create_canned_response( template="Sure thing.", tags=[p.Tag.preamble()], )

p.Tag.preamble()对应 SDK 中的 Tag.preamble 工厂方法。源码中的行为值得注意:在strict 模式下,preamble 只能从带preambletag 的 canned responses 中渲染选取(#L817-L874),且若 LLM 输出的 preamble 不在候选列表中会被直接丢弃并记录错误日志(#L897-L902)——strict 模式对前导语同样"零自由"。非 strict 模式下,preamble 由 LLM 参考一组默认示例(如 "Just a moment"、"Let me check that for you" 等,#L3000-L3007)就近生成。带preambletag 的响应在正文检索阶段会被显式排除(#L945-L953),不会与正文候选混淆。

五、模板语法:三类字段与 Jinja2

Canned responses 以**模板(template)**定义。模板是字符串,可包含静态文本,以及在选择后被实际值替换的动态字段。

标准字段(std. 前缀)

使用std.前缀引用对话上下文中的动态信息。文档列出的可用值:

  1. std.customer.name:String;客户姓名(未注册客户为Guest);
  2. std.agent.name:String;Agent 名称;
  3. std.variables.NAME:Any;名为NAME的变量内容;
  4. std.missing_params:String 列表;基于工具洞察(Tool Insights)得到的缺失工具参数名列表。
await agent.create_canned_response( template="Hi {{std.customer.name}}, Yes, this product is available in stock." )

从源码结构看,StandardFieldExtraction 实际暴露的std命名空间比文档列表更宽:除customer.nameagent.namevariables.*missing_params外,还包括std.invalid_params(参数名到非法取值的映射)与std.glossary(术语名到定义的映射)。如果你需要在模板中提示"你提供的 XX 无效"或引用术语定义,可以直接利用这些字段。

生成式字段(generative. 前缀)

引用generative.前缀的字段时,LLM 会基于字段名与上下文自动推断并替换其值。这是在严格模板中引入受控的、局部生成的利器:

await agent.create_canned_response( template="Can I ask why you'd like to return {{generative.item_name}}?" )

实现上,GenerativeFieldExtraction 用正则\{\{(generative\.[a-zA-Z0-9_]+)\}\}从模板中提取字段名,为每个字段构造一个专门的字段抽取 prompt(包含 Agent 身份、上下文变量、命中的 guidelines、交互历史、glossary 与暂存工具事件),要求 LLM 输出一个可"干净地嵌回模板"的值;所有字段生成失败任何一个,整条模板即渲染失败并被剔除(#L334-L348),因此 generative 字段不会产出半成品句子。

工具/检索器字段(Tool/Retriever-Based Fields)

Canned responses 还可以引用来自工具和检索器结果的字段,字段必须声明在ToolResultRetrieverResultcanned_response_fields属性中。这是最有用的字段类型,因为它能把真正动态的数据引入 canned responses:

@p.tool def get_account_balance(context: p.ToolContext) -> p.ToolResult: balance = 1234.5 return p.ToolResult( # 注意:仍需在 `data` 字段中提供结果, # 因为它会在 Agent 评估 guidelines、调用工具以及生成草稿消息时起作用。 data={f"Account balance is {balance}"}, # 这里提供专门用于模板字段替换的动态值 canned_response_fields={"account_balance": balance}, )

模板引用方式:

await agent.create_canned_response(template="Your current balance is {{account_balance}}")

字段在防幻觉中的关键作用

警告:字段对避免"后果性幻觉"至关重要

使用工具字段还有一个重要收益:检索候选响应时,引擎会同时参考canned_response_fields判断相关性。

引用了上下文中不存在字段的响应永远不会被选中——即使它与草稿消息高度相似。这确保了 Agent 输出的响应锚定在真实可用的数据上。

例如,在 strict 模式下,如果successful_transaction字段没有被某个成功运行的工具调用提供,你的 Agent 就永远不会输出任何引用{{successful_transaction.id}}的消息。换句话说,只要响应与工具协调得当,你就能确保 Agent 绝不就数据或状态幻觉出误导性回复。

这条"字段依赖门控"在源码中可精确验证:_get_relevant_canned_responses 会先汇总会话中所有工具调用结果里的canned_response_fields键(外加stdgenerative与额外注入字段),得到fields_available_in_context;然后用jinja2.meta.find_undeclared_variables解析每条模板的全部变量(_get_response_template_fields,#L498-L501),只有模板的全部字段都出现在上下文中,该模板才进入候选(#L1009-L1016)。这是向量检索之外的第二道硬过滤,是 strict 模式下"引用不存在的交易 ID 就绝不发送"这一保证的底层机制。

六、从工具直接返回完整响应

工具不仅可以提供字段值,还可以直接返回完整的 canned response 候选——当你希望基于工具输出生成一条完整回复(而非仅提供数据供字段替换)时特别有用,通常出现在复杂的 Q&A 检索场景中:

@p.tool def get_answer(context: p.ToolContext, question: str) -> p.ToolResult: answer = "The answer to your question is...." return p.ToolResult( data=answer, # 将该答案作为完整的 canned response 候选提供 canned_responses=[answer], )

源码中,这类响应以"瞬态(transient)"身份参与候选:_get_relevant_canned_responses 从暂存工具事件中读取canned_responses列表,用CannedResponse.create_transient包装后并入候选集;且瞬态响应不受字段依赖过滤与向量相似度阈值约束(#L2280-L2284),保证本轮工具刚产出的答案一定会参与最终选择。

七、优化响应选择:控制草稿 + Signals

选择质量的上限由草稿质量决定。要确保 Agent 选到正确的 canned response,可以分两步优化:

1. 控制草稿消息

由于选择过程以草稿为基准,第一步是让草稿尽可能贴近你期望的响应。为此可以使用全部标准控制手段:guidelines、journeys、tools、glossary 术语、Agent 描述。这意味着你需要密切关注选择前生成的草稿——在 Parlant 集成的 UI 中可以直接检查生成的草稿消息,看 Agent"本想说什么"。

2. 用 Signals 确保正确的候选被检索到

有时响应本身与草稿足够接近,能自然出现在候选列表里;但并不总是如此——尤其当模板含有字段替换、语义相似度比较变难时。

这时可以使用signals:告诉 Agent"这些草稿样式适合匹配这条响应"。每条 signal 本质上就是一个草稿消息示例。检索候选时,引擎会同时参考这些 signals 判断相关性;只要某条响应拥有一条与草稿消息非常接近的 signal,即使响应本身形态差异很大,它也会被检索为候选:

await agent.create_canned_response( template="Yes, we've got this item in stock! Let me know if you need any help finding it.", signals=["We do have it in stock", "We do! Do you need help finding it?"], )

底层实现印证了这一点:CannedResponseVectorStore._insert_canned_response 在入库时会把模板文本与每一条 signal分别嵌入向量(_list_canned_response_contents返回[value, *signals],#L535-L536);检索时 _list... / filter_relevant_canned_responses 按canned_response_id去重并取最近距离,因此"模板 + signals"共同构成该话术的检索面。这是解决"模板里{{...}}太多、原文向量与草稿不相似"问题的正规手段。

八、Jinja2 的灵活性

响应模板集成了 Jinja2 模板引擎,支持更动态的格式化、替换过滤器(filters)与列表处理,高级语法可参考 Jinja2 官方文档。例如配合工具字段渲染列表:

@p.tool def get_pizza_toppings(context: p.ToolContext) -> p.ToolResult: toppings = ['olives', 'peppers', 'onions'] return p.ToolResult( data={f"Toppings are {toppings}"}, canned_response_fields={"toppings": toppings}, )
await agent.create_canned_response( template="We have the following toppings {% for t in toppings %}\n- {{t}}{% endfor %}" )

渲染环节由jinja2.Template(response.value).render(**args)完成(#L2473),字段值由字段抽取器按std → 工具 → 附加字段 → generative的优先级链解析(CannedResponseFieldExtractor)。

九、No-Match Responses:Strict 模式的兜底

在 strict 模式下,如果 Agent 无法为草稿找到合适的 canned response,就会发送一条 no-match 响应。默认值来自 canned_response_generator.py#L82:"Not sure I understand. Could you please say that another way?"。自定义有两种方式:

静态 No-Match 响应

最简单:设置一条静态模板,Agent 找不到合适 canned response 时一律使用它:

async def initialize_func(c: p.Container) -> None: no_match_provider = c[p.BasicNoMatchResponseProvider] no_match_provider.template = "My custom no-match response." async with p.Server( initialize_container=initialize_func, ) as server: ...

对应 BasicNoMatchResponseProvider:它只是把template属性原样返回,因此完全静态、零 LLM 开销。

自定义 No-Match Provider

需要更多灵活性时,可以实现自定义 provider,基于对话上下文动态生成 no-match 响应:

注意:p.LoadedContext(提供内部引擎状态访问)可能在后续版本中变化,你的实现将来可能需要相应调整。

class CustomNoMatchResponseProvider(p.NoMatchResponseProvider): async def get_template(self, context: p.LoadedContext, draft: str | None) -> str: # 基于提供的上下文生成自定义 no-match 响应, # 例如对话历史、草稿消息、guidelines、工具调用等 template = "..." return template async def configure_func(c: p.Container) -> p.Container: c[p.NoMatchResponseProvider] = CustomNoMatchResponseProvider() async with p.Server( configure_container=configure_func, ) as server: ...

抽象基类 NoMatchResponseProvider 的get_response会把get_template的返回值包装为瞬态CannedResponse;从调用点看,draft参数在无候选可检索时传入None(#L2202-L2210),在"有候选但匹配质量不足"时传入实际草稿(#L2364-L2366)——因此动态 provider 可以据此区分"完全没有可用话术"与"有话术但都不贴切"两种场景,给出更贴切的引导语。

十、要点小结

  • 机制:canned responses 是"草稿 → 向量检索 → Jinja2 渲染 → LLM 选择"的四阶段流水线(canned_response_generator.py),草稿决定选择质量,模板决定输出边界;
  • 模式:Fluid 保流畅、Composited 仿风格、Strict 零幻觉;可 Agent 级设置,也可被 guideline 级模式局部收紧(最严格者优先);
  • 字段std.取上下文标准值(源码中还含invalid_paramsglossary),generative.做受控局部生成,工具/检索器字段注入真实业务数据,并以"字段缺失即不入选"的依赖门控杜绝后果性幻觉;
  • 检索:模板与 signals 共同嵌入向量库(canned_responses.py),候选上限 30 条、相似度阈值 0.4;
  • 兜底:strict 模式的 no-match 可用BasicNoMatchResponseProvider.template静态化,或继承NoMatchResponseProvider动态生成;
  • 验证:端到端行为有 strict_canned_responses.feature 等 Gherkin 场景与 tests/api/test_canned_responses.py 的 API 测试可作参考,便于你本地复现与回归验证。

按此路径,你可以先以 strict mode + 少量高置信模板起步,用 UI 中可见的草稿消息持续校准 guidelines 与 signals,再视风险逐步放开到 composited 或 fluid——整个过程中对话模型的其他构件始终可复用。

【免费下载链接】parlantBuild reliable customer-facing AI agents with Parlant: an interaction control harness optimized for controlled, consistent, and predictable LLM interactions.项目地址: https://gitcode.com/GitHub_Trending/pa/parlant

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询