你们有没有遇到过这种情况:Agent 每执行一步都要去调第三方 API,令牌在工具函数里传了一圈,最后不知怎么就打到日志里了。我最早做多工具 Agent 的时候,就是这么把生产环境的 API Key 打满了一整个日志面板。标题里这个“Google ADK-Python 安全令牌传递机制”,说白了就是在用 ADK-Python 搭 Agent 的时候,把 API Key、OAuth 令牌、服务账号凭证这类敏感身份信息,安全地在 Agent 内部、工具函数之间、外部服务之间传递的一套玩法。它解决的问题很具体:凭证不硬编码、不落日志、不超范围授权。适合正在用 ADK-Python 做 Agent 开发的读者,哪怕你只是刚接触 Agent,这套思路也能直接拿过去抄。1. Agent 令牌传递机制,拆开看就是“身份穿越”1.1 工具调用链里被忽略的“身份穿越”问题要先理解这个问题,得先看 Agent 的本质。Agent 程序不是一条直线执行到底的,它是“规划-执行-反馈”的循环。LLM 大模型先理解用户请求,拆成多个步骤,然后每个步骤去调用一个工具,工具返回结果,再喂给模型做下一步决策。这个过程中,每调用一个工具,都可能要访问一个受保护的服务。比如一个检索类 Agent:用户问“帮我查项目上周的构建报告”,Agent 内部实际要依次请求权限校验服务、日志聚合服务、对象存储服务。每个服务都要确认“你是谁、你能看什么”,这时候令牌就得一直跟着请求走。我把这个过程叫“身份穿越”,因为令牌不是静态坐在某一个函数里的,它要穿过 Agent 的每一次工具调用的边界。一个工具拿到令牌做了某件事,下一个工具可能要用同一份令牌去访问另一个服务,也可能要根据服务差异换成另一份令牌。更现实的问题是:有的工具调用是串行的,有的工具调用是并行的,令牌在并发场景下怎么保证不串、不丢、不重复刷新,这就是机制要解决的问题。类比一下现实场景:一个大型公司的访客机制。员工带访客去不同楼层,每到一个区域,前台要验证访客牌的权限级别,决定放不放行。如果访客牌只有一个万能权限,整个办公区随便进出,一旦丢失,灾难。Agent 的令牌传递也是同一个道理,区别只是把“前台验证”变成了服务端的鉴权中间件,把“访客牌”变成了 Access Token。你设计得好,令牌在每层都只暴露必要权限;设计得不好,一个万能钥匙满天飞。1.2 硬编码凭据为什么一定走不远很多人写第一版 Agent 的时候,代码都是这个画风:API_KEY = "sk-xxxx-xxxx" SERVICE_ACCOUNT = "svc-report-reader@example"然后需要调用服务的地方直接引用这个全局变量。先不说代码仓库权限控制得怎么样,单是“全局变量 + Agent 多步调用”这个组合就够危险的。问题一,泄露面太大。Agent 的完整调用链里往往包含模型输入、模型输出、工具入参、工具出参、日志、监控上报,任何一个环节把变量内容带出来,全局变量里的密钥就出去了。我自己踩过最典型的一个坑是:工具函数把请求 URL 拼错了,异常消息里直接带了 Authorization header 的完整值,然后异常被链路追踪系统收集,密钥就这么“合法地”进了监控平台。问题二,权限过大。一把 Key 能读对象存储,也能写对象存储,还能调管理接口。Agent 本来只需要读一份报告,结果 Key 的权限范围是“全部读写”。万一来一次提示词注入,让 Agent 调用了不该调用的工具,多余权限直接变成真实风险。问题三,没法审计。谁在什么时间用了哪个令牌去访问什么服务,这个信息完全没有记录,出了问题根本没法追溯。1.3 ADK-Python 是怎么设计这件事的ADK-Python 是搭建 Agent 应用的框架,核心概念包括 Agent、Tool、Session、Context。它的设计里有一个很关键的思路:Agent 执行过程中的状态不是散落在全局变量里,而是封装在一个上下文对象中,这个上下文会贯穿整个调用链,从 Agent 启动到每个工具执行,都能访问到同一份数据。令牌这种敏感信息,就应该放在这个上下文里。这里要说明一下,ADK-Python 的 API 在不同版本里字段名可能会调整,下面涉及的代码是我基于常见实践做的合理示例,重点是思路。核心思路是三层:第一层是启动时统一注入凭证,Agent 入口从一个安全的凭证源拿到所有需要的令牌;第二层是上下文携带,令牌通过上下文对象传给当前这一步要执行的工具函数;第三层是工具内部按需取用,每个工具只从上下文里取自己服务对应的那一个凭证,而不是全局拿一把万能 Key。这个设计比起“全局变量 + 到处 import”,最大的好处是:凭证的生命周期、可见范围、审计点都是可控的。2. 令牌传递的四种常见模式,以及怎么选2.1 四种主流令牌模式的对比做 Agent 令牌传递,首先不是写代码,而是选模式。我整理了一下实际项目里最常见的四种:令牌模式典型场景有效期轮换难度风险等级静态 API Key内部服务间调用、测试环境长期有效高中高OAuth 2.0 令牌交换Agent 模拟用户操作、访问用户资源短时令牌 + 刷新令牌中中服务账号委托后台任务、无人值守的定时 Agent证书签发、短期令牌中低临时 STS 凭证云资源访问、跨账号授权分钟级到小时级低低静态 API Key 最直接,配置一个密钥字符串,Agent 启动时读进来。缺点是长期有效,一旦泄露就要人工吊销重发,整个过程要等配置生效、各缓存过期,挺费劲。OAuth 2.0 的基本逻辑是:用户先授权给 Agent,Agent 拿到一个短时 Access Token 和 Refresh Token,Access Token 过期后用 Refresh Token 换新的。服务账号委托则是 Agent 自己有一个身份,不需要用户参与,适合定时任务。临时 STS 凭证是云平台提供的受限临时身份,用完就失效,权限范围可以由参数严格限定,适合跨账号拿资源的场景。2.2 选型思路:先看“信任边界”我个人的选型经验是:先别管技术栈,先回答三个问题。第一个问题:Agent 是代表用户操作,还是代表自己操作?代表用户,选 OAuth 2.0 令牌交换;代表自己,选服务账号或静态 Key。第二个问题:Agent 要访问的资源和环境是怎么隔离的?如果生产环境和测试环境用的是同一个账号体系,再好的令牌机制也是白搭,信任边界先在环境层面划清楚。第三个问题:令牌泄露之后,你最怕什么?是怕范围过大被人利用,还是怕无法及时吊销?如果最怕是范围过大,那临时 STS 凭证就比静态 Key 合适得多。这里我想强调一个经验:不要为了省事,把所有外部服务都塞进同一个 OAuth scope 里。scope 是 API 用来做“最小权限”的天然工具,你给 Agent 申请权限的时候,scope 列表就是你的第一道防线。我用过一个项目:Agent 要读对象存储里的文件,也要写另一个 bucket,结果申请令牌的时候图省事直接申请了“全部资源管理权限”。后面做权限收紧时,为了改这个 scope,所有 Agent 实例要重新换取令牌,光协调各服务方就花了两天。如果一开始就按服务拆分 scope,收紧是几分钟的事。2.3 一个容易被忽略的“令牌缓存与刷新”设计选完模式之后,还有个实际问题:令牌不能每次都重新去换,也不能永远不换,得有个缓存和刷新机制。缓存的目的很明确:服务端签发的令牌通常有时效,频繁换取会给认证服务造成压力,而且每次换取都是一次网络往返,会拖慢 Agent 的响应速度。刷新策略这块,我踩过的最典型的坑是:多个工具并行执行,同时发现令牌过期,同时去刷新令牌,结果认证服务收到一堆相同请求,其中一部分返回了刷新失败。解决方案是加一个进程内的互斥锁,保证同一时刻只有一个刷新动作在跑,其他调用等待刷新完成之后直接拿新令牌。3. 实操:在 ADK-Python 里落地安全令牌传递3.1 先把目录结构和凭证模块搭起来代码落地前,先规划一下目录。最简单的结构是这样:agent_workspace/ ├── credential_manager.py # 凭证管理:缓存、刷新、脱敏 ├── agents.py # Agent 定义与工具注册 ├── tools/ │ ├── report_tool.py # 查报告的工具 │ └── storage_tool.py # 读存储的工具 └── main.py # 程序入口凭证管理模块是整个机制的地基。它的职责是:启动时从环境变量或密钥管理服务读取基础凭证,按服务名缓存当前有效的 Access Token,提供加锁的刷新逻辑,并且只在内存中保留令牌明文。模块本身不负责具体业务逻辑,不主动打印任何包含令牌的内容。这样设计的好处是:Agent 的其他模块根本不需要知道令牌从哪来、怎么刷新的细节,它们只需要调用credential_manager.get_token(service_name)就能拿到当前可用的令牌。3.2 使用工具函数解耦令牌传递ADK-Python 里工具函数是 Agent 能力的载体。每个工具函数都接收一个上下文参数,令牌就放在上下文里。代码示例:# tools/report_tool.py from google.adk import tool @tool def fetch_build_report(date: str, ctx) -> dict: """拉取指定日期的构建报告。""" cred = ctx.credentials["report_service"] headers = {"Authorization": f"Bearer {cred.access_token}"} url = f"https://report-internal.example/v1/builds/{date}" response = http_get(url, headers=headers, timeout=10) return response.json()这里有几个关键点。第一,工具函数里没有从环境变量直接读取令牌,而是从ctx.credentials里面拿,这样令牌来源和刷新逻辑都被集中管理了。第二,某个工具只取自己服务对应的凭证,而不是全局遍历所有凭证,这可以避免把无关服务的令牌带进当前调用链。第三,工具函数返回的是业务结果,不含任何令牌信息。3.3 连接受保护服务的完整流程示例下面这个示例是模拟场景,但流程上和我实际跑过的项目几乎一致。假设有两个工具:fetch_build_report负责从报告服务拉数据,fetch_report_file负责从对象存储下载原始文件。Agent 收到用户请求“把最新构建报告和原始日志文件打包下载”时,会串行调用这两个工具。凭证管理模块先定义服务注册表:# credential_manager.py import threading import time class CredentialManager: def __init__(self): self._tokens = {} self._lock = threading.Lock() def get_token(self, service: str) -> str: with self._lock: cred = self._tokens.get(service) if cred and cred["expires_at"] > time.time() + 60: return cred["access_token"] return self._refresh(service) def _refresh(self, service: str) -> str: # 这里是统一的刷新逻辑,只从安全配置源读取刷新凭据 # 实际项目中可以从密钥管理服务拉取 pass然后在 Agent 启动时,把当前用到的服务列表注册进去:# main.py from credential_manager import CredentialManager from agents import build_report_agent cm = CredentialManager() cm.register("report_service", refresh_token_env="REPORT_SERVICE_REFRESH") cm.register("storage_service", refresh_token_env="STORAGE_SERVICE_REFRESH") agent = build_report_agent(credential_manager=cm) result = agent.run("把最新构建报告和原始日志文件打包下载")工具函数通过上下文拿到令牌后,发请求、取数据、返回结果。整体调用链就是:Agent 入口 → 上下文携带 credentials → 工具按需取用 → 服务返回结果 → 上下文继续传给下一个工具。这个链路中最容易出问题的是“上下文里的 credentials 从哪初始化”这一步,必须在一个统一入口做,不能每个工具自己初始化一份。3.4 日志脱敏与审计的落地写法令牌最容易泄露的两个出口,一个是异常堆栈,一个是日志。处理办法有两层。第一层是在日志输出层做脱敏,只要字段名包含token、key、secret、authorization这些关键词,一律替换成掩码。第二层是在凭证管理模块返回令牌时就带上类型标记,这样工具打印字典、序列化响应时,能自动识别敏感字段。代码示例:# log_sanitizer.py import re SENSITIVE_PATTERN = re.compile( r"(token|api_key|secret|authorization|password)", re.IGNORECASE, ) def sanitize(value, mask="***"): if isinstance(value, str) and SENSITIVE_PATTERN.search(value): return mask if isinstance(value, dict): return {k: sanitize(v) for k, v in value.items()} return value审计方面,不需要搞复杂的系统,先把两个字段记下来就足够:某个工具调用时用的是哪个 service 的令牌、这个令牌的 scope 是什么。这两个字段能支撑绝大多数排查场景。我自己在项目里会在每次凭证刷新时打一条结构化日志,包含服务名、新令牌的过期时间、scope 列表,但绝对不打令牌本身。4. 常见问题与排查技巧实录4.1 令牌在工具间“平白消失”这是我被问得最多的一个问题:第一个工具里还能取到ctx.credentials,第二个工具里就变成空字典了。排查的时候先别怀疑框架,先看工具函数的参数声明。ADK-Python 这类框架里,工具函数要显式接收上下文参数。如果你在中间某一层没有把上下文透传下去,或者把工具包装成了闭包,上下文链路就断了,令牌自然就“消失”了。我的习惯是:写工具函数时,第一个参数就放ctx,并且不随意改参数名,保持整个项目一致。还有一个隐藏坑:有些工具框架做了线程池调度,上下文对象不是线程安全的。如果工具实现里有异步回调,要先在主线程里把令牌取出来,再传给回调函数,不要在回调里反复读上下文。4.2 日志把令牌打了出去这类问题我见过的版本很多:有人把请求对象整个放进日志里,有人把 headers 字典打印了出来,还有人把异常对象直接序列化。最狠的一次是,一个工具函数把完整响应体打到了日志,响应体里包含服务端回显的 Access Token,那一行日志在监控系统里躺了一周。处理思路是:日志模块必须统一,接一个结构化日志器,所有 stdout 输出都走这个模块。然后在每个 Agent 工具入口加一个统一的脱敏装饰器,凡是工具函数的入参和返回值,进出去之前都过一遍脱敏逻辑。不用做得太复杂,正则清洗加上关键词掩码就够用了。4.3 刷新令牌并发冲突这个问题前面提过。现象是:Agent 里多个工具并行调用,某个瞬间所有工具都发现当前令牌过期,于是同时去刷新。刷新服务那边可能没有做幂等处理,结果其中一个刷新请求成功了,另外几个拿到的是旧刷新凭据换出来的新令牌,甚至直接报“refresh token already used”。解决办法就是加锁。注意这个锁必须是进程内的互斥锁,而不是分布式锁。如果是多实例部署,每个实例只对自己的内存缓存负责,分布式锁反而会把问题搞复杂。如果你用的是多进程部署,可以给不同进程分配不同的服务实例,或者接受每个进程独立维护一份令牌缓存,前提是刷新凭据本身是可重复使用的。4.4 权限“收不回”还有一个普遍头疼的问题:令牌配置成了“万能权限”,真要出事时无从下手。我建议做 Agent 项目的时候,专门做一次权限清单表,列出每个工具用到的服务、令牌类型、scope、过期策略。这个表不一定要给外部系统看,主要是自己排查用。每次新增一个工具,先更新这个表,再写代码。这个习惯帮我避过一次大坑:有一次要给 Agent 增加一个“删除旧归档”的功能,我看到权限清单上对应服务只有只读 scope,立刻意识到这个工具不能直接接进来,需要先申请新的受限 scope。多花了两天申请流程,但避免了 Agent 在生产环境拥有删除权限的风险。5. 最后分享一点我的实践体会做这个令牌传递机制,我实际的体会是:安全性和开发效率往往是对着干的,但令牌传递这件事,恰恰是两者能同时兼顾的少数环节。你只要把凭证源统一到一个模块、把传递通道统一到上下文、把日志出口统一做脱敏,后面再加工具、加服务,基本不需要改动既有逻辑,开发效率反而会变得更高。我最早做的时候也嫌这套东西繁琐,毕竟刚开始 Agent 只有两个工具,直接写全局变量最省事。后来工具数量一多,服务一多,当初省下的那点功夫连半天排查日志泄露都不够用。建议正在做 ADK-Python Agent 开发的朋友,哪怕场景再简单,也先把这套机制搭起来。先让令牌只在你规定的通道里流动,再在最小权限的基础上逐步放开,这比一开始放开后面再收紧要省太多力气。最后再补一句:令牌传递机制不是一次性的,每次新增工具类型、每次调整服务权限范围,都应该回头检查一遍上下文里到底装了什么、日志里可能带出什么。好记性不如烂笔头,把权限清单和上下文结构写进项目文档,比什么都管用。
你们有没有遇到过这种情况:Agent 每执行一步都要去调第三方 API,令牌在工具函数里传了一圈,最后不知怎么就打到日志里了。我最早做多工具 Agent 的时候,就是这么把生产环境的 API Key 打满了一整个日志面板。标题里这个“Google ADK-Python 安全令牌传递机制”,说白了就是在用 ADK-Python 搭 Agent 的时候,把 API Key、OAuth 令牌、服务账号凭证这类敏感身份信息,安全地在 Agent 内部、工具函数之间、外部服务之间传递的一套玩法。它解决的问题很具体:凭证不硬编码、不落日志、不超范围授权。适合正在用 ADK-Python 做 Agent 开发的读者,哪怕你只是刚接触 Agent,这套思路也能直接拿过去抄。1. Agent 令牌传递机制,拆开看就是“身份穿越”1.1 工具调用链里被忽略的“身份穿越”问题要先理解这个问题,得先看 Agent 的本质。Agent 程序不是一条直线执行到底的,它是“规划-执行-反馈”的循环。LLM 大模型先理解用户请求,拆成多个步骤,然后每个步骤去调用一个工具,工具返回结果,再喂给模型做下一步决策。这个过程中,每调用一个工具,都可能要访问一个受保护的服务。比如一个检索类 Agent:用户问“帮我查项目上周的构建报告”,Agent 内部实际要依次请求权限校验服务、日志聚合服务、对象存储服务。每个服务都要确认“你是谁、你能看什么”,这时候令牌就得一直跟着请求走。我把这个过程叫“身份穿越”,因为令牌不是静态坐在某一个函数里的,它要穿过 Agent 的每一次工具调用的边界。一个工具拿到令牌做了某件事,下一个工具可能要用同一份令牌去访问另一个服务,也可能要根据服务差异换成另一份令牌。更现实的问题是:有的工具调用是串行的,有的工具调用是并行的,令牌在并发场景下怎么保证不串、不丢、不重复刷新,这就是机制要解决的问题。类比一下现实场景:一个大型公司的访客机制。员工带访客去不同楼层,每到一个区域,前台要验证访客牌的权限级别,决定放不放行。如果访客牌只有一个万能权限,整个办公区随便进出,一旦丢失,灾难。Agent 的令牌传递也是同一个道理,区别只是把“前台验证”变成了服务端的鉴权中间件,把“访客牌”变成了 Access Token。你设计得好,令牌在每层都只暴露必要权限;设计得不好,一个万能钥匙满天飞。1.2 硬编码凭据为什么一定走不远很多人写第一版 Agent 的时候,代码都是这个画风:API_KEY = "sk-xxxx-xxxx" SERVICE_ACCOUNT = "svc-report-reader@example"然后需要调用服务的地方直接引用这个全局变量。先不说代码仓库权限控制得怎么样,单是“全局变量 + Agent 多步调用”这个组合就够危险的。问题一,泄露面太大。Agent 的完整调用链里往往包含模型输入、模型输出、工具入参、工具出参、日志、监控上报,任何一个环节把变量内容带出来,全局变量里的密钥就出去了。我自己踩过最典型的一个坑是:工具函数把请求 URL 拼错了,异常消息里直接带了 Authorization header 的完整值,然后异常被链路追踪系统收集,密钥就这么“合法地”进了监控平台。问题二,权限过大。一把 Key 能读对象存储,也能写对象存储,还能调管理接口。Agent 本来只需要读一份报告,结果 Key 的权限范围是“全部读写”。万一来一次提示词注入,让 Agent 调用了不该调用的工具,多余权限直接变成真实风险。问题三,没法审计。谁在什么时间用了哪个令牌去访问什么服务,这个信息完全没有记录,出了问题根本没法追溯。1.3 ADK-Python 是怎么设计这件事的ADK-Python 是搭建 Agent 应用的框架,核心概念包括 Agent、Tool、Session、Context。它的设计里有一个很关键的思路:Agent 执行过程中的状态不是散落在全局变量里,而是封装在一个上下文对象中,这个上下文会贯穿整个调用链,从 Agent 启动到每个工具执行,都能访问到同一份数据。令牌这种敏感信息,就应该放在这个上下文里。这里要说明一下,ADK-Python 的 API 在不同版本里字段名可能会调整,下面涉及的代码是我基于常见实践做的合理示例,重点是思路。核心思路是三层:第一层是启动时统一注入凭证,Agent 入口从一个安全的凭证源拿到所有需要的令牌;第二层是上下文携带,令牌通过上下文对象传给当前这一步要执行的工具函数;第三层是工具内部按需取用,每个工具只从上下文里取自己服务对应的那一个凭证,而不是全局拿一把万能 Key。这个设计比起“全局变量 + 到处 import”,最大的好处是:凭证的生命周期、可见范围、审计点都是可控的。2. 令牌传递的四种常见模式,以及怎么选2.1 四种主流令牌模式的对比做 Agent 令牌传递,首先不是写代码,而是选模式。我整理了一下实际项目里最常见的四种:令牌模式典型场景有效期轮换难度风险等级静态 API Key内部服务间调用、测试环境长期有效高中高OAuth 2.0 令牌交换Agent 模拟用户操作、访问用户资源短时令牌 + 刷新令牌中中服务账号委托后台任务、无人值守的定时 Agent证书签发、短期令牌中低临时 STS 凭证云资源访问、跨账号授权分钟级到小时级低低静态 API Key 最直接,配置一个密钥字符串,Agent 启动时读进来。缺点是长期有效,一旦泄露就要人工吊销重发,整个过程要等配置生效、各缓存过期,挺费劲。OAuth 2.0 的基本逻辑是:用户先授权给 Agent,Agent 拿到一个短时 Access Token 和 Refresh Token,Access Token 过期后用 Refresh Token 换新的。服务账号委托则是 Agent 自己有一个身份,不需要用户参与,适合定时任务。临时 STS 凭证是云平台提供的受限临时身份,用完就失效,权限范围可以由参数严格限定,适合跨账号拿资源的场景。2.2 选型思路:先看“信任边界”我个人的选型经验是:先别管技术栈,先回答三个问题。第一个问题:Agent 是代表用户操作,还是代表自己操作?代表用户,选 OAuth 2.0 令牌交换;代表自己,选服务账号或静态 Key。第二个问题:Agent 要访问的资源和环境是怎么隔离的?如果生产环境和测试环境用的是同一个账号体系,再好的令牌机制也是白搭,信任边界先在环境层面划清楚。第三个问题:令牌泄露之后,你最怕什么?是怕范围过大被人利用,还是怕无法及时吊销?如果最怕是范围过大,那临时 STS 凭证就比静态 Key 合适得多。这里我想强调一个经验:不要为了省事,把所有外部服务都塞进同一个 OAuth scope 里。scope 是 API 用来做“最小权限”的天然工具,你给 Agent 申请权限的时候,scope 列表就是你的第一道防线。我用过一个项目:Agent 要读对象存储里的文件,也要写另一个 bucket,结果申请令牌的时候图省事直接申请了“全部资源管理权限”。后面做权限收紧时,为了改这个 scope,所有 Agent 实例要重新换取令牌,光协调各服务方就花了两天。如果一开始就按服务拆分 scope,收紧是几分钟的事。2.3 一个容易被忽略的“令牌缓存与刷新”设计选完模式之后,还有个实际问题:令牌不能每次都重新去换,也不能永远不换,得有个缓存和刷新机制。缓存的目的很明确:服务端签发的令牌通常有时效,频繁换取会给认证服务造成压力,而且每次换取都是一次网络往返,会拖慢 Agent 的响应速度。刷新策略这块,我踩过的最典型的坑是:多个工具并行执行,同时发现令牌过期,同时去刷新令牌,结果认证服务收到一堆相同请求,其中一部分返回了刷新失败。解决方案是加一个进程内的互斥锁,保证同一时刻只有一个刷新动作在跑,其他调用等待刷新完成之后直接拿新令牌。3. 实操:在 ADK-Python 里落地安全令牌传递3.1 先把目录结构和凭证模块搭起来代码落地前,先规划一下目录。最简单的结构是这样:agent_workspace/ ├── credential_manager.py # 凭证管理:缓存、刷新、脱敏 ├── agents.py # Agent 定义与工具注册 ├── tools/ │ ├── report_tool.py # 查报告的工具 │ └── storage_tool.py # 读存储的工具 └── main.py # 程序入口凭证管理模块是整个机制的地基。它的职责是:启动时从环境变量或密钥管理服务读取基础凭证,按服务名缓存当前有效的 Access Token,提供加锁的刷新逻辑,并且只在内存中保留令牌明文。模块本身不负责具体业务逻辑,不主动打印任何包含令牌的内容。这样设计的好处是:Agent 的其他模块根本不需要知道令牌从哪来、怎么刷新的细节,它们只需要调用credential_manager.get_token(service_name)就能拿到当前可用的令牌。3.2 使用工具函数解耦令牌传递ADK-Python 里工具函数是 Agent 能力的载体。每个工具函数都接收一个上下文参数,令牌就放在上下文里。代码示例:# tools/report_tool.py from google.adk import tool @tool def fetch_build_report(date: str, ctx) -> dict: """拉取指定日期的构建报告。""" cred = ctx.credentials["report_service"] headers = {"Authorization": f"Bearer {cred.access_token}"} url = f"https://report-internal.example/v1/builds/{date}" response = http_get(url, headers=headers, timeout=10) return response.json()这里有几个关键点。第一,工具函数里没有从环境变量直接读取令牌,而是从ctx.credentials里面拿,这样令牌来源和刷新逻辑都被集中管理了。第二,某个工具只取自己服务对应的凭证,而不是全局遍历所有凭证,这可以避免把无关服务的令牌带进当前调用链。第三,工具函数返回的是业务结果,不含任何令牌信息。3.3 连接受保护服务的完整流程示例下面这个示例是模拟场景,但流程上和我实际跑过的项目几乎一致。假设有两个工具:fetch_build_report负责从报告服务拉数据,fetch_report_file负责从对象存储下载原始文件。Agent 收到用户请求“把最新构建报告和原始日志文件打包下载”时,会串行调用这两个工具。凭证管理模块先定义服务注册表:# credential_manager.py import threading import time class CredentialManager: def __init__(self): self._tokens = {} self._lock = threading.Lock() def get_token(self, service: str) -> str: with self._lock: cred = self._tokens.get(service) if cred and cred["expires_at"] > time.time() + 60: return cred["access_token"] return self._refresh(service) def _refresh(self, service: str) -> str: # 这里是统一的刷新逻辑,只从安全配置源读取刷新凭据 # 实际项目中可以从密钥管理服务拉取 pass然后在 Agent 启动时,把当前用到的服务列表注册进去:# main.py from credential_manager import CredentialManager from agents import build_report_agent cm = CredentialManager() cm.register("report_service", refresh_token_env="REPORT_SERVICE_REFRESH") cm.register("storage_service", refresh_token_env="STORAGE_SERVICE_REFRESH") agent = build_report_agent(credential_manager=cm) result = agent.run("把最新构建报告和原始日志文件打包下载")工具函数通过上下文拿到令牌后,发请求、取数据、返回结果。整体调用链就是:Agent 入口 → 上下文携带 credentials → 工具按需取用 → 服务返回结果 → 上下文继续传给下一个工具。这个链路中最容易出问题的是“上下文里的 credentials 从哪初始化”这一步,必须在一个统一入口做,不能每个工具自己初始化一份。3.4 日志脱敏与审计的落地写法令牌最容易泄露的两个出口,一个是异常堆栈,一个是日志。处理办法有两层。第一层是在日志输出层做脱敏,只要字段名包含token、key、secret、authorization这些关键词,一律替换成掩码。第二层是在凭证管理模块返回令牌时就带上类型标记,这样工具打印字典、序列化响应时,能自动识别敏感字段。代码示例:# log_sanitizer.py import re SENSITIVE_PATTERN = re.compile( r"(token|api_key|secret|authorization|password)", re.IGNORECASE, ) def sanitize(value, mask="***"): if isinstance(value, str) and SENSITIVE_PATTERN.search(value): return mask if isinstance(value, dict): return {k: sanitize(v) for k, v in value.items()} return value审计方面,不需要搞复杂的系统,先把两个字段记下来就足够:某个工具调用时用的是哪个 service 的令牌、这个令牌的 scope 是什么。这两个字段能支撑绝大多数排查场景。我自己在项目里会在每次凭证刷新时打一条结构化日志,包含服务名、新令牌的过期时间、scope 列表,但绝对不打令牌本身。4. 常见问题与排查技巧实录4.1 令牌在工具间“平白消失”这是我被问得最多的一个问题:第一个工具里还能取到ctx.credentials,第二个工具里就变成空字典了。排查的时候先别怀疑框架,先看工具函数的参数声明。ADK-Python 这类框架里,工具函数要显式接收上下文参数。如果你在中间某一层没有把上下文透传下去,或者把工具包装成了闭包,上下文链路就断了,令牌自然就“消失”了。我的习惯是:写工具函数时,第一个参数就放ctx,并且不随意改参数名,保持整个项目一致。还有一个隐藏坑:有些工具框架做了线程池调度,上下文对象不是线程安全的。如果工具实现里有异步回调,要先在主线程里把令牌取出来,再传给回调函数,不要在回调里反复读上下文。4.2 日志把令牌打了出去这类问题我见过的版本很多:有人把请求对象整个放进日志里,有人把 headers 字典打印了出来,还有人把异常对象直接序列化。最狠的一次是,一个工具函数把完整响应体打到了日志,响应体里包含服务端回显的 Access Token,那一行日志在监控系统里躺了一周。处理思路是:日志模块必须统一,接一个结构化日志器,所有 stdout 输出都走这个模块。然后在每个 Agent 工具入口加一个统一的脱敏装饰器,凡是工具函数的入参和返回值,进出去之前都过一遍脱敏逻辑。不用做得太复杂,正则清洗加上关键词掩码就够用了。4.3 刷新令牌并发冲突这个问题前面提过。现象是:Agent 里多个工具并行调用,某个瞬间所有工具都发现当前令牌过期,于是同时去刷新。刷新服务那边可能没有做幂等处理,结果其中一个刷新请求成功了,另外几个拿到的是旧刷新凭据换出来的新令牌,甚至直接报“refresh token already used”。解决办法就是加锁。注意这个锁必须是进程内的互斥锁,而不是分布式锁。如果是多实例部署,每个实例只对自己的内存缓存负责,分布式锁反而会把问题搞复杂。如果你用的是多进程部署,可以给不同进程分配不同的服务实例,或者接受每个进程独立维护一份令牌缓存,前提是刷新凭据本身是可重复使用的。4.4 权限“收不回”还有一个普遍头疼的问题:令牌配置成了“万能权限”,真要出事时无从下手。我建议做 Agent 项目的时候,专门做一次权限清单表,列出每个工具用到的服务、令牌类型、scope、过期策略。这个表不一定要给外部系统看,主要是自己排查用。每次新增一个工具,先更新这个表,再写代码。这个习惯帮我避过一次大坑:有一次要给 Agent 增加一个“删除旧归档”的功能,我看到权限清单上对应服务只有只读 scope,立刻意识到这个工具不能直接接进来,需要先申请新的受限 scope。多花了两天申请流程,但避免了 Agent 在生产环境拥有删除权限的风险。5. 最后分享一点我的实践体会做这个令牌传递机制,我实际的体会是:安全性和开发效率往往是对着干的,但令牌传递这件事,恰恰是两者能同时兼顾的少数环节。你只要把凭证源统一到一个模块、把传递通道统一到上下文、把日志出口统一做脱敏,后面再加工具、加服务,基本不需要改动既有逻辑,开发效率反而会变得更高。我最早做的时候也嫌这套东西繁琐,毕竟刚开始 Agent 只有两个工具,直接写全局变量最省事。后来工具数量一多,服务一多,当初省下的那点功夫连半天排查日志泄露都不够用。建议正在做 ADK-Python Agent 开发的朋友,哪怕场景再简单,也先把这套机制搭起来。先让令牌只在你规定的通道里流动,再在最小权限的基础上逐步放开,这比一开始放开后面再收紧要省太多力气。最后再补一句:令牌传递机制不是一次性的,每次新增工具类型、每次调整服务权限范围,都应该回头检查一遍上下文里到底装了什么、日志里可能带出什么。好记性不如烂笔头,把权限清单和上下文结构写进项目文档,比什么都管用。