资讯详情

资讯详情

ADK 指令动态化实战:深入解析 inject_session_state 占位符注入与模板引擎

ADK 指令动态化实战深入解析 inject_session_state 占位符注入与模板引擎【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-pythoninject_session_state是 Google ADKAgent Development Kit中负责把指令模板里的{user_name}之类占位符替换为会话状态值与工件内容的公共函数它同时驱动了 ADK 内置的默认花括号引擎与可选的 Jinja2 渲染引擎。本文以该函数为核心讲解其在 ADK 中的四种内置调用时机、占位符三种形态的差异、源码级的工作原理、动态指令对上下文缓存的影响以及如何在你自己的指令函数InstructionProvider中手动调用它以恢复占位符行为——读完即可在生产代码中正确使用动态指令。为什么需要指令注入一个 Agent 的指令instruction通常是常量字符串可一旦它需要提及当前对话中的信息——用户的名字、几分钟前上传的文件、三轮之前设定的偏好——就必须从会话状态session state动态组装。ADK 通过inject_session_state完成这种组装但只覆盖了两种指令提供方式中的一种这个差异常常让人踩坑传入普通字符串如instructionHello {user_name}.ADK 会在每次模型调用前对字符串执行inject_session_state因此{user_name}会被替换为会话状态里的实际值传入可调用对象callable如instructionbuild_instructionADK 调用你的函数并将其返回值原样作为指令。此处的状态注入被刻意跳过——框架假设一个能读取上下文的函数可以自己完成插值。第二种情况正是inject_session_state被设为公共 API 的原因如果你的指令函数返回的字符串里含有{user_name}没有任何机制会替你去替换模型看到的将是带花括号的原文。此时手动调用inject_session_state就是把占位符行为找回来的唯一方式。InstructionProvider就是这类可调用指令的类型别名其定义为Callable[[ReadonlyContext], str | Awaitable[str]]与inject_session_state声明在同一模块中并一同导出声明位置见 instructions_utils.py。在动手使用占位符之前还有一个需要权衡的点解析后的指令会成为系统指令system instruction而上下文缓存context cache是以包含该指令的前缀为键的。因此一段文本会随请求变化的指令会直接导致缓存失效详见后文「动态指令对上下文缓存的代价」一节。快速上手在自定义指令函数中手动注入下面的例子来自 官方文档 的入门章节编写一个既做自己的逻辑、又能解析占位符的指令提供器。from google.adk.agents import Agent from google.adk.agents.readonly_context import ReadonlyContext from google.adk.utils.instructions_utils import inject_session_state async def build_instruction(readonly_context: ReadonlyContext) - str: base You are a support agent. if readonly_context.state.get(escalated): base This case has been escalated; be brief and precise. return await inject_session_state( base The customer is {customer_name} on plan {plan_tier?}., readonly_context, ) root_agent Agent( namesupport_agent, descriptionAnswers customer support questions., instructionbuild_instruction, )当会话状态中包含{customer_name: Ada}而没有plan_tier时模型实际收到的指令为You are a support agent. The customer is Ada on plan .——因为plan_tier?末尾的问号会把缺失的值转换为空字符串而非抛错。注意inject_session_state是一个async函数必须在await之后使用build_instruction本身返回Awaitable[str]正好满足InstructionProvider的类型要求。ReadonlyContext的核心能力在 readonly_context.py 中可以看到它通过只读视图暴露state、session、agent_name等字段inject_session_state正是经由它内部的_invocation_context拿到会话与工件服务的。占位符语法详解默认的正则引擎识别三种形态它们之间真正要紧的区别是取值缺失时的行为——这正是语法本身不会直接告诉你的部分。{name}必填形式引擎在会话状态中查找name并替换为str()之后的值。如果键不在状态中会在请求中途抛出KeyError——当指令在没有该值时毫无意义这正是你想要的行为。一条You are helping {customer_name}.的指令与其告诉模型它在帮助某个无名之人不如直接失败。{name?}可选形式末尾的问号会在查找前从键名中剥离缺失的键替换为空字符串而不是抛错。只要某个键只是偶尔被设置就应当使用这种形式。代价是占位符所在的句子必须在值缺失时依然读得通。例如The customer is on plan {plan_tier?}.在缺失时会渲染成The customer is on plan .所以把值单独放一行之类的写法值得多花一点心思。{artifact.filename}加载工件而不是状态值引擎向工件服务artifact service请求当前会话中的该工件并替换为str()之后的内容。工件缺失时抛KeyError{artifact.filename?}则与可选状态形式一样替换为空字符串。在依赖这种形式之前务必先确认str()的输出是什么从工件服务取回的工件是一个types.Part它的str()是整个 Pydantic 字段的 dump而不是你脑子里想的纯文本详见「局限性」一节。状态作用域前缀状态键可以携带状态前缀因此{app:theme}、{user:locale}、{temp:draft}都能工作并分别从对应作用域读取。各前缀的具体含义参见 State 指南。测试时有一个坑需要注意通过create_session(state...)传入的temp:键会被丢弃因为临时状态从不持久化{temp:draft}只有在某次调用真正写入它之后才能解析成功。None 与 str() 渲染状态值为None时渲染为空字符串而不是文本None其余所有值都用str()渲染。含花括号的普通文本不受影响替换之前引擎会先检查花括号内容是否长得像状态名一个 Python 标识符可选的带app:、user:、temp:前缀。任何不符合的都原样返回因此指令中作为 JSON 示例出现的{role: user}或作为空对象的{}都会完整保留。这个检查看的是形状而非是否存在{customer_name}在状态里没有customer_name时是一个合法但缺失的名字会抛错而{customer name}含空格不是合法名字会原样留在提示词里。工作原理从源码看正则引擎inject_session_state通过你传入的ReadonlyContext触达会话与工件服务因此你只能在存在上下文的地方调用它——指令提供器、插件回调、工具内都是典型场景。其实现位于 instructions_utils.py引擎用_TEMPLATE_VAR_PATTERN re.compile(r{[^{}]*})匹配「一串左花括号 一段不含花括号的文本 一串右花括号」匹配到的内容两端的花括号被剥掉剩余部分做strip()模板里完全没有{时直接原样返回——这是静态指令的常见情况每次模型调用零开销源码 instructions_utils.py 的注释明确说明这是为了避免在每次 LLM 调用时做正则扫描替换是单次从左到右的遍历因此值本身若含有花括号会被原样插入绝不会被再次扫描。对每个匹配引擎在三种情形间决策以artifact.开头的名字触发artifact_service.load_artifact如果 runner 没有工件服务抛出ValueError(Artifact service is not initialized.)不是合法状态名的名字保持原样其余名字在会话状态中查找。_is_valid_state_nameinstructions_utils.py实现了合法性检查无前缀时要求isidentifier()带前缀时只允许app:、user:、temp:三个作用域前缀且冒号后的部分也必须是合法标识符。框架已经在哪些地方替你调用它了解以下四处调用点你就能知道占位符在哪里生效、在哪里不生效。它们都可以在源码中找到对应证据每个字符串形式的instruction以及根 Agent 上的字符串global_instruction在每次模型调用前注入。核心实现在 instructions.py_process_agent_instruction调用agent.canonical_instruction(...)拿到原始指令后只要bypass_state_injection为假就执行inject_session_state。绝大多数 Agent 依赖的就是这第一个调用点。另外当static_instruction与动态instruction同时存在时动态指令会被_label_dynamic_instruction包上BEGIN_SYSTEM_INSTRUCTION标记并作为用户内容contents送入请求以保证静态前缀字节稳定、便于上下文缓存匹配——这从另一个侧面印证了「动态指令不进缓存前缀」的设计意图应用app级别的字符串 global instructionGlobalInstructionPlugin在 global_instruction_plugin.py 中区分字符串与InstructionProvider字符串走inject_session_statecallable 则直接调用后原样返回ManagedAgent的系统指令见 _managed_agent.py同样遵循「callable 跳过注入」的规则frontmatter 设置了metadata.adk_inject_state: true的 Skill 正文在模型加载该 Skill 的时刻注入见 skill_toolset.py。在上述前三种场景中只要指令来自 callable注入就会被跳过。另外LlmAgent的static_instruction从不经过这个函数——它被设计为完全不做替换、原样发送的静态内容。动态指令对上下文缓存的代价解析后的指令就是 Agent 的系统指令而上下文缓存以包含它的前缀为键。所以问题不在于你的指令有没有占位符而在于占位符背后的值变不变如果占位符每次请求都解析成同样的文本比如设置一次就不再动的{user:locale}系统指令保持稳定缓存持续命中如果解析结果随请求变化比如每个用户不同的客户名、每一轮都在变的值系统指令就不再匹配缓存构建时的前缀该请求就要按全价重新支付整个前缀的 token。同理适用于任何按请求重写系统内容的行为——模板、回调、插件甚至ExampleTool这类工具。想保住缓存可以把指令拆成两半把永远不变的文本放进static_instruction作为系统指令发送完全不做替换把占位符留在instruction里作为普通用户内容随请求发送。这样变化的文本依然到达模型但不再位于被缓存的前缀内部。缓存本身的开关通过App.context_cache_config配置详见 App 指南。配置参数速查表inject_session_state接受两个必填参数和一个能彻底切换模板语言的开关签名见 instructions_utils.py选项类型默认值说明templatestr必填含占位符的指令文本。readonly_contextReadonlyContext必填提供会话状态与工件服务。use_jinja2boolFalse使用 Jinja2 而非默认花括号引擎渲染。高级应用一Jinja2 条件与循环花括号引擎只能做替换它无法按条件包含某个段落也无法遍历状态里的列表。因此当指令需要改变形状而非改变某个词时需要真正的模板语言。传入use_jinja2True后会话状态键成为模板的顶层变量工件则通过异步的artifact()辅助函数加载async def build_instruction(readonly_context: ReadonlyContext) - str: return await inject_session_state( {% if show_hint is defined and show_hint %}Hint: read the docs.{% endif %} {% for item in items %}{{ item }} {% endfor %}, readonly_context, use_jinja2True, )两个引擎除了共用这一个函数之外毫无交集语法不同{{ var }}对{var}{{ artifact(report.md) }}对{artifact.report.md}缺失值的行为也不同。ADK 以undefinedjinja2.StrictUndefined构建 Jinja2 环境instructions_utils.py因此不在会话状态中的变量会抛错而非渲染为空也没有?的等价物缺失工件在两个引擎中都会抛KeyError。自动转义autoescape关闭这对提示词文本是正确的选择——若用于 HTML 则是错的。StrictUndefined 陷阱StrictUndefined正是上面示例中出现is defined的原因。裸写{% if show_hint %}在show_hint缺失时同样会抛错因为对未定义名字做真值测试本身就足以触发它jinja2.exceptions.UndefinedError: show_hint is undefined。这与常规 Jinja2 的行为相反很容易坑到人——「用if保护可选键」恰恰是行不通的做法。请写成{% if x is defined and x %}或者保证该键一定在状态中。Jinja2 是可选的依赖Jinja2 不是 ADK 基础安装的一部分它随 evaluation 与 testing extras 一起分发。在 pyproject.toml 中jinja23.1.4,4出现在 extras 依赖清单里。在未安装 jinja2 的情况下调用use_jinja2True会抛出ImportError提示你pip install jinja2。导入发生在函数内部而非模块顶层instructions_utils.py因此仅仅 import ADK 永远不会强依赖它——test_instructions_utils.py 中有专门测试验证「无 jinja2 时模块可正常导入、调用时抛ImportError」。高级应用二渲染非指令文本同样的占位符语法经常被用在工具描述、提示词片段、或即将写入工件artifact的文本中。这个函数并不与「指令」绑定——只要手握ReadonlyContextToolContext也是其中之一你就可以对任意字符串调用它。这意味着团队可以把「按会话状态插值」这一能力复用到任何需要拼接动态文本的地方而无需复制实现。局限性一览缺失键默认抛错。{name}在状态中没有name时会在请求中途抛KeyError。若该键只是偶尔出现{name?}几乎总是你想要的。不支持嵌套花括号且静默失败。内部花括号永远不会被匹配在{outer{inner}}中唯一的匹配是{inner}}结尾的一串右花括号被它一并消耗。当inner为I时整体渲染为{outerI——没有错误开头花括号仍留在提示词里。无递归。包含{other}的替换值会被原样插入。拼写错误静默。{costumer_name}是合法但缺失的状态名会抛错{customer name}不是合法名字会原样留在提示词中让模型看到花括号。两种拼写错误以完全不同的方式失败。工件被字符串化而这几乎从不是你想要的结果。两个引擎都插入str(artifact)而工件服务加载的工件是types.Part其str()是 Pydantic 字段 dump。一个内容为 hello world 的纯文本工件到达提示词时是media_resolutionNone code_execution_resultNone ... texthello world thoughtNone ...——Part 的所有字段而你只想要其中一个。二进制数据更糟。没有提取.text的选项因此需要工件内容的指令应在 Python 中加载并自行插值文本而不是使用{artifact.name}。callable 指令不注入。这是刻意设计——假设能访问上下文的函数可以自行插值——也正是需要你手动调用本函数的原因。请求之间变化的值会击穿上下文缓存。解析后的指令就是系统指令占位符值一动就会改变缓存前缀下一次请求缓存失效。把不变文本挪进static_instruction。以上行为均有 test_instructions_utils.py 中的用例佐证例如缺失状态抛KeyError匹配Context variable not found: ...、缺失工件抛KeyError、非法名字原样返回、None渲染为空串、可选形式渲染为空串、Jinja2 条件/循环/工件/过滤器、未定义变量抛错等可作为你理解与回归验证的参考。实战示例Skill 状态注入官方文档指向的示例 skills_inject_state 展示了如何让一个SKILL.md的正文走一遍本函数。其 code-review-skill/SKILL.md 的 frontmatter 设置了metadata.adk_inject_state: true正文中使用{dev_name?}、{dev_language?}、{dev_level?}这类可选占位符把开发者档案从会话状态注入到代码评审指令中档案为空时 Skill 会先引导用户自我介绍。这正是「字符串指令 → 框架自动注入」路径在真实场景下的完整落地。相关指南State 指南会话状态里装什么以及app:、user:、temp:前缀的含义BaseArtifactService{artifact.name}从何处加载ManagedAgent 指南框架调用点之一也是 callable 指令跳过注入的又一实例。【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →