ADK-python 技能会话状态注入实战:用 adk_inject_state 让 SKILL.md 动态读取 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-python
导读
本指南基于 ADK(Agent Development Kit)官方示例 skills_inject_state,系统讲解如何通过SKILL.mdfrontmatter 中的一行声明metadata.adk_inject_state: true,让 Skill 在加载时自动把会话状态(Session State)中的值注入到指令模板中,实现「同一个 Skill、不同会话、不同个性化指令」。读完本文,你将掌握{key}、{key?}、{user:key}等占位符语法、完整的示例工程拆解、底层注入实现原理,以及状态新鲜度相关的正确使用姿势。
为什么需要状态注入:告别「Getter 工具 + 额外一次 LLM 往返」
在 Agent 应用里,Skill 经常需要读取 Agent 已经持有的信息——用户偏好、对话上下文、某个配置值等。在引入adk_inject_state之前,一个需要读取状态的 Skill 通常要走下面这条笨重链路:
- 为 Skill 单独编写一个自定义的 "getter" 工具,专门负责从状态里取值;
- 通过
SkillToolset(additional_tools=[...])把这个工具挂接到 Skill 上; - 在 Skill 指令中指示模型先调用这个 getter 工具,拿到值之后再执行真正的任务。
这条链路带来了双重开销:多写一份应用代码(getter 工具本身),以及运行时多一次 LLM 往返(模型必须实际调用该工具才能读到状态)。对一个本该专注于「按规则干活」的 Skill 来说,这是不必要的样板代码。
adk_inject_state正是为消除这份样板而设计的声明式方案:当 Skill 的SKILL.mdfrontmatter 设置了metadata.adk_inject_state: true,LoadSkillTool会在加载 Skill 时通过inject_session_state渲染 Skill 正文,把其中的{placeholder}替换为会话状态中对应的值。这套{var}/{var?}插值语法与LlmAgent.instruction所支持的完全一致(见 instructions_utils.py),如今被扩展到了 Skill 场景,只需一行声明式改动即可启用。
示例概览:一个会「认人」的代码审查 Skill
官方示例 agent.py 构建了一个名为skills_inject_state_agent的 Agent,它展示的核心能力有四点:
- 选择注入(Opt-in):在
SKILL.md的 frontmatter 中设置metadata.adk_inject_state: true; - 声明式状态访问:在 Skill 正文里直接用
{dev_name}、{dev_language}、{dev_level}占位符引用会话状态,无需任何 getter 工具; - 状态填充:一个
remember_developer_profile工具把开发者档案写入会话状态,Skill 稍后通过注入读取; - 状态新鲜度理解:状态值只在 Skill 加载时物化(materialize)一次;加载之后的状态变更不会影响已加载的 Skill,除非重新加载。
工作流程
graph TD User -->|"1. introduces themselves"| Agent[Agent: skills_inject_state_agent] Agent -->|writes dev_name, dev_language, dev_level| State[(Session State)] User -->|"2. asks for a code review"| Agent Agent -->|load_skill code-review-skill| Toolset[SkillToolset] State -. injected into instructions .-> Toolset Toolset -->|instructions with state filled in| Agent第一步,用户在会话中自我介绍,Agent 调用remember_developer_profile把档案写入状态;第二步,用户请求代码审查,Agent 通过load_skill加载code-review-skill,由于该 Skill 已声明状态注入,占位符在指令返回前就已经被状态值填充完毕——全程没有额外的工具调用。
示例工程拆解:Agent 与 SKILL.md 的双向配合
Agent 侧:写状态与挂 Skill
在 agent.py 中,remember_developer_profile是一个带tool_context: ToolContext参数的普通函数工具,通过tool_context.state["dev_name"]等方式把档案写入会话状态:
def remember_developer_profile( name: str, primary_language: str, experience_level: str, tool_context: ToolContext, ) -> dict: """Saves the developer's profile into session state for later personalization.""" tool_context.state["dev_name"] = name tool_context.state["dev_language"] = primary_language tool_context.state["dev_level"] = experience_level return { "status": "ok", "stored": { "dev_name": name, "dev_language": primary_language, "dev_level": experience_level, }, }随后用load_skill_from_dir加载目录型 Skill,装入SkillToolset,并挂到根 Agent 上:
code_review_skill = load_skill_from_dir( pathlib.Path(__file__).parent / "skills" / "code-review-skill" ) my_skill_toolset = SkillToolset(skills=[code_review_skill]) root_agent = Agent( name="skills_inject_state_agent", description=( "An agent that personalizes a code-review skill using session state." ), instruction=( "You help developers review their code.\n" "- When a user introduces themselves, call" " `remember_developer_profile` to save who they are.\n" "- When a user asks for a code review, load the `code-review-skill`" " and follow its (personalized) instructions exactly." ), tools=[ remember_developer_profile, my_skill_toolset, ], )注意这里的指令设计:Agent 被明确指示「用户自我介绍时调用remember_developer_profile存档」「用户请求代码审查时加载code-review-skill」。这保证了状态在 Skill 加载之前就已经就位,与「先写状态、再加载 Skill」的正确时序完全吻合。
Skill 侧:一行声明开启注入
SKILL.md 的 frontmatter 是注入开关所在:
--- name: code-review-skill description: Reviews code with feedback tailored to the developer's profile in session state. metadata: adk_inject_state: true ---正文使用可选的占位符形式引用状态,并用「档案为空则先询问」的逻辑保证优雅降级:
You are performing a personalized code review. The developer's profile (from session state): - Name: {dev_name?} - Primary language: {dev_language?} - Experience level: {dev_level?} If the profile above is empty, first ask the developer to introduce themselves (their name, primary language, and experience level) so the review can be personalized, then stop. Otherwise, follow these steps: 1. Greet the developer by name. 2. Review the code the user provided, focusing on idioms and best practices for their primary language. 3. Calibrate the depth of your feedback to their experience level: keep it foundational for a junior developer, and concise and advanced for a senior developer. 4. End with one concrete, actionable suggestion.这份 Skill 的「个性化」完全来自状态注入:同一份模板,对 Alex(Python / senior)与对新人开发者(初级 / 某语言)会渲染出完全不同的审查指令,而 Skill 文件本身无需任何改动。
运行示例:两种输入回合
在示例的父目录下启动:
adk web然后在同一个会话中按顺序发送以下两轮消息:
第 1 轮:Hi, I'm Alex. I mainly write Python and I'm a senior engineer.
效果:Agent 调用
remember_developer_profile,将档案写入会话状态。
第 2 轮:Can you review this for me? def add(a, b): return a+b
效果:Agent 加载
code-review-skill。因为 Skill 声明了adk_inject_state,{dev_name}/{dev_language}/{dev_level}占位符在指令返回时已从状态填充完毕——读取档案不再需要额外的工具调用。
占位符语法详解:从{key}到{user:key}
占位符与会话状态的键一一对应,支持的语法在 instructions_utils.py 的替换逻辑中实现:
| 语法 | 含义 | 键缺失时的行为 |
|---|---|---|
{key} | 必填占位符,读取当前会话状态中的key | 注入失败,抛出KeyError |
{key?} | 可选占位符,读取key | 替换为空字符串,不报错 |
{user:key} | 读取 user 作用域前缀状态 | 同{key}规则 |
{app:key} | 读取 app 作用域前缀状态 | 同{key}规则 |
{temp:key} | 读取 temp 作用域前缀状态 | 同{key}规则 |
此外,源码还支持{artifact.file_name}:将 Skill/指令中引用的 artifact 内容注入进来(artifact 不存在且为可选形式时替换为空串,否则抛KeyError)。
值得注意的底层细节:
- 校验规则:
_is_valid_state_name(见 instructions_utils.py)只接受合法标识符,或user:/app:/temp:前缀加合法标识符;不合法的{var}会被原样保留(直接返回匹配原文),而不是报错或静默删除; - 值类型:状态值为
None时替换为空字符串,其余值经str()转成字符串后注入; - Frontmatter 类型校验:
adk_inject_state必须是布尔值,否则Frontmatter模型会抛出ValueError("adk_inject_state must be a bool")(见 models.py)。
本示例刻意使用可选形式({dev_name?}等),这样在档案尚未写入时加载 Skill 会优雅降级——指令中对应字段留空,Skill 转而执行「先请用户自我介绍」的逻辑,而不是直接报错中断。
源码级原理:LoadSkillTool 与 inject_session_state 的调用链
adk_inject_state的实现横跨两个模块:
1. Skill 加载时触发注入:SkillToolset内部的LoadSkillTool.run_async(见 skill_toolset.py)在取到 Skill 的指令后检查 frontmatter:
instructions = skill.instructions if skill.frontmatter.metadata.get("adk_inject_state"): instructions = await instructions_utils.inject_session_state( instructions, tool_context, )即:LoadSkillTool把 Skill 正文当作模板,交给inject_session_state渲染,渲染结果作为load_skill的返回值之一(instructions字段)返回给模型,进入对话上下文。
2. 统一的模板引擎:inject_session_state(见 instructions_utils.py)与LlmAgent.instruction用的是同一套插值逻辑:
- 默认走基于正则的
_render_with_regex引擎({+[^{}]*}+模式),逐段异步替换; - 可选
use_jinja2=True切换到 Jinja2 渲染,支持条件与循环等更丰富的模板语法,此时会话状态变量可直接按名访问({{ var_name }}),artifact 通过{{ artifact("file_name") }}异步加载——注意 Jinja2 是可选依赖,需pip install jinja2; - 替换时从
invocation_context.session.state读取状态,因此注入内容天然与会话隔离:不同会话的状态不同,同一 Skill 渲染出的指令也不同。
这条调用链印证了文档中的关键结论:注入发生在加载时(load time),且只发生一次。渲染完成后的指令已经固化在对话上下文中,后续状态变化不会自动刷新。
状态新鲜度与最佳实践
这是使用adk_inject_state最容易踩坑、也最需要牢记的部分:
- 加载时物化:状态值在
load_skill被调用的那一刻解析并注入一次。如果之后会话状态发生变化,已经返回进对话上下文的指令不会自动更新——它不是「活引用」; - 先写状态,再加载 Skill:务必保证 Skill 所需的会话状态值在模型加载 Skill 之前就已就位。这也是示例中 Agent 指令刻意设计为「先自我介绍存档、后请求审查」的原因;
- 动态或高频变化的状态:对于在执行过程中持续变化的值,优先使用常规的 getter 工具调用,或显式重新加载 Skill,而不是依赖加载时的一次性注入。
adk_inject_state适合的是低频、会话级、个性化上下文(用户偏好、身份档案、配置项),而不是任务运行中不断变动的临时数据。
进阶阅读
- 完整示例代码与 Skill 文件:skills_inject_state 示例目录
- Skill 数据模型与 frontmatter 校验:models.py
- 注入触发点
LoadSkillTool:skill_toolset.py - 模板引擎
inject_session_state与状态名校验:instructions_utils.py - Skill 官方指南:Skill 使用文档 与 Skill Registry 文档
【免费下载链接】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),仅供参考