同一套AI Agent代码,你在本地跑得好好的,部署上去后却表现判若两人——回答变慢、上下文串场、输出时粗时细。你反复检查Prompt和工具函数,都没发现问题,其实很可能是“运行配置”这一层出了状况。
在Google的ADK(Agent Development Kit)里,这一层被集中抽象成了Runtime Config,也就是RunConfig。它用一组配置项回答了“这个Agent在被调用时,究竟应该怎么跑”:用哪个模型、生成参数是多少、会话状态怎么存、要不要缓存、超时和指标怎么处理。把这些配置清楚之后,你的Agent才谈得上稳定、可控、可复现。
这篇文章适合三类读者:刚开始用ADK搭Agent、想让Agent从“能跑”变成“好跑”的开发者;遇到Agent行为不稳定但查不出逻辑问题的实践者;以及准备把Agent部署到线上、需要控制成本与延迟的工程师。我会尽量把RunConfig的每一块面板都讲明白,并附上可以直接抄的配置示例。
1. 先理解一件事:RunConfig到底在配置“什么”
1.1 一次真实的现象:同样的Agent,两种表现
先讲一个我在实际调试中遇到的例子。
我做了一个用于售后答疑的Agent,核心逻辑很简单:根据用户描述找到问题分类,然后调用知识库工具给出解决方案。Prompt写得挺标准,工具函数的输入输出也调通了。本地跑一遍测试用例,全过。然后部署到测试环境,跑了不到半天就暴露出一堆问题:有两个用户问相似的问题,第二个用户得到的答案里居然混进了第一个用户的历史信息;同一个问题连续问两遍,第二次Agent“换了个说法”;最长的一次回答直接超时。
第一反应是模型不稳定,或者是知识库接口波动。后来把日志翻出来才明白,跟这些都没有关系,真正的原因是运行时这一层太“裸”了——会话状态没有正确隔离、生成参数没设、工具结果缓存策略缺失,结果Agent在一个不受控的“野”状态下运行。
这其实特别典型。很多人第一次用ADK写Agent,把注意力全放在Agent的名字、模型和指令上,忽略了RunConfig。代码能跑,但“怎么跑”完全不受控。
1.2 RunConfig是驾驶规则,不是发动机
我习惯用一个类比来理解RunConfig和Agent逻辑的关系。
Agent本身可以看作一辆车。Prompt是发动机,工具函数是轮子,这些都是“硬件”层面的东西,决定了这辆车能跑多快、能走什么路。而RunConfig是驾驶规则——用几挡起步、什么时候换挡、每个路口怎么转向、要不要开巡航、油表什么时候报警。同一辆车,让不同驾驶规则去开,表现完全不同。
放在ADK的语境下:
- Prompt负责“Agent是什么、懂什么”
- 工具负责“Agent能做什么”
- RunConfig负责“Agent被调用时,每一步具体怎么跑”
1.3 RunConfig挂在哪里:和Agent、Runner的关系
在ADK里,RunConfig不是孤立的一块,它分布在不同的配置入口:
- Agent初始化时:model、instruction、generation_config、tools、run_config等
- Runner初始化时:session_service、artifact_service等
- Session创建时:状态存储方式
RunConfig和Runner的分工大致是:Agent层的配置决定了单个Agent内部的生成行为;Runner层的配置决定了Agent和外部世界的交互方式(怎么接收消息、怎么保持会话、怎么返回事件流)。
如果落到代码上:
from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService agent = Agent( name="support_agent", model="gemini-2.0-flash", instruction="你是一名售后客服,始终基于知识库内容回答,不做猜测。", ) session_service = InMemorySessionService() runner = Runner( agent=agent, app_name="support_app", session_service=session_service, )上面这段代码里,Agent的名字、模型、指令是Agent的核心配置,而Runner里的session_service就是运行时配置的一部分。RunConfig要做的,就是把这些“怎么跑”的参数统一规划好,而不是想到哪个设哪个。
2. 逐个旋钮拆解:RunConfig核心配置项详解
2.1 模型选择:model与endpoint
第一个绕不开的配置就是模型。ADK把模型选择做成了Agent的一个标准参数:
agent = Agent( name="my_agent", model="gemini-2.0-flash", )这里model指定了Agent推理时用的模型。我提一个容易忽略的点:ADK的Kotlin版本目前内置模型只支持Gemini系列(这也是我在项目里看到的一个明确限制),而Python版本可以通过endpoint等配置连接不同的模型服务,灵活性更高。如果你在Kotlin工程里想换非Gemini模型,大概率是要自己扩展适配层了。
model的参数选择要结合任务复杂度:
- 简单的分类、抽取、格式化任务,用响应快的轻量模型
- 复杂的推理、规划、多步工具调用,用能力更强的模型
endpoint参数决定了模型服务的访问地址。默认情况下,SDK会用内置的默认endpoint。只有在走自定义API网关、私有化部署或者做本地Model Runner测试的时候,才需要显式设置。这里我多说一句:千万别在生产环境随便改endpoint,除非你确实有一个稳定的自定义网关在背后。
对了,很多人遇到“agent execution terminated due to error.”这类错误,其实很多就跟endpoint配置指向不可服务的地址有关。先看endpoint,再看认证,最后才怀疑Prompt。
2.2 生成质量:temperature、top_p、max_output_tokens
生成参数是调节“Agent输出风格”最直接的一批旋钮。在ADK中,GenerationConfig(或generation_config)就是干这个的:
agent = Agent( name="creative_writer", model="gemini-2.0-flash", generation_config={ "temperature": 0.7, "top_p": 0.95, "max_output_tokens": 2048, }, )- temperature:控制随机性。数值越接近0,输出越确定、越稳定;数值越高,越有发散和创造力。对客服、工单分类这类任务,建议0到0.3之间;对创意文案、头脑风暴,0.7到0.9能明显看出效果差异。
- top_p:核采样,模型只在累计概率达到top_p的候选token里选。通常和temperature配合使用,基础配置里设0.9-0.95就够了。
- max_output_tokens:单次输出的最大token数。如果不设,模型会按默认上限生成。做对接时一定要检查这个值,否则长答案会被截断,轻则内容不完整,重则JSON解析失败。
Kotlin版本里对应的是GenerationConfig类,参数名是驼峰形式,比如maxOutputTokens,语义完全一样。
不同任务场景的推荐配置,我列个参考表:
| 任务类型 | temperature | top_p | max_output_tokens | 配置要点 |
|---|---|---|---|---|
| 客服/售后 | 0.0-0.3 | 0.9 | 256-512 | 稳定优先,避免编造 |
| 内容创作 | 0.7-0.9 | 0.95 | 2048以上 | 创意优先,允许发散 |
| 数据抽取/格式化 | 0.0-0.2 | 0.9 | 1024-2048 | 精确优先,输出可控 |
这里有个实操建议:把“任务类型”和“生成参数”绑定,做成一份场景化的配置模板。客服Agent一定用低temperature加较短的max_output_tokens,确保回复稳定且不啰嗦;创意Agent则反过来。不要让所有Agent共用一套默认参数,否则一定会出现“该稳定的不稳定、该发散的太干瘪”的尴尬情况。
2.3 记忆机制:会话服务与状态持久化
Agent单次生成模型只能“看到”当前上下文,但Agent产品是多轮对话的,所以会话状态必须由运行时来管。ADK里这块的叫法是SessionService。
from google.adk.sessions import InMemorySessionService session_service = InMemorySessionService()InMemorySessionService把会话直接放在内存里,优点是快、零配置,缺点是进程一停,所有会话跟着消失。所以它的定位是本地调试和Demo,不是生产。
生产环境要接入持久化的SessionService实现,把session存到数据库或分布式存储里。这样Agent重启、扩容之后,用户的历史上下文还能继续使用。
然后就是RunConfig里的session配置块:主要定义了会话的存储位置、会话ID的生成规则、上下文清理策略。常见做法是每次用户请求都带上同一个session_id,Agent自然就能读到历史。
我遇到过一个很隐蔽的失忆问题:底层模型是支持长上下文的,但会话服务配置了过短的历史截断,导致多轮之后Agent“忘了”前面的关键信息。排查时不会直接报错,只表现为回答质量突然下降。所以会话配置里,上下文保留策略一定要和模型的上下文窗口做匹配。
2.4 成本与速度:缓存策略
RunConfig里的caching配置很多人一开始完全忽略。因为Agent逻辑写出来之后,本地反复调试,每次都要真实调用模型,既慢又费token。ADK支持对请求和工具结果做缓存。
典型收益场景:
- 工具函数的返回结果基本稳定,比如天气查询、价格查询
- 同一个用户短时间内反复触发同一个工具
- 相同上下文的重试请求
配置了缓存之后,相同的输入直接命中缓存,不再重复调用模型或工具。这个对生产环境的成本和延迟优化非常明显。
需要注意缓存失效问题。缓存不是万能的,如果工具结果本身就是变化的(比如实时汇率),还去加缓存,用户会拿到过时的数据。我的经验是:只对“结果确定性高、时效要求低”的工具开缓存,对实时性敏感的工具,要么不缓存,要么设置很短的过期时间。
2.5 其他值得注意的参数
RunConfig还会涉及几个不那么显眼、但可能决定上线成败的参数:
- 认证配置(auth):Agent在调用受限资源时的身份凭证。多Agent协作时每个子Agent可能有不同的权限,这时候授权配置要安排明白。
- 指标收集(metrics):把每一次调用耗时、token消耗、错误类型打点输出。这个在排查问题的时候价值极高。
- 实时事件(live events):决定Agent执行过程中的中间事件要不要实时推给客户端。做流式输出和进度展示时会用到。
这些参数日常调试可能用不上,但一到上线评估阶段就是必选项。我建议从第一天就把metrics打开,不要等到线上出问题再去补。
3. 实战:从最小可用到多Agent协作的配置演进
3.1 第一步:最小可用配置
先用最短的代码把Agent跑起来。
from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService agent = Agent( name="basic_agent", model="gemini-2.0-flash", instruction="你是一个友好的助手。", ) session_service = InMemorySessionService() runner = Runner( agent=agent, app_name="basic_app", session_service=session_service, ) session = session_service.create_session( app_name="basic_app", user_id="u001", session_id="s001", ) events = runner.run( user_id="u001", session_id="s001", message="你好,介绍一下你自己。", ) for event in events: if event.content: print(event.content)这段代码没有任何显式的RunConfig,跑起来直接、干净。它的作用是验证链路通不通,虽然在生产上是不可用的,但在学习阶段,这是最正确的第一步。
3.2 第二步:针对真实场景调整生成参数
假设现在要做一个售后客服Agent。需求很明确:回答必须基于知识库,不能自由发挥;回复要简洁;多轮对话要记得用户之前的技术问题。
那配置就会变成:
from google.adk.agents import Agent agent = Agent( name="support_agent", model="gemini-2.0-flash", instruction=( "你是售后客服助手。请严格根据知识库文档回答用户问题。" "不确定的内容就说明不确定,不要编造。回复控制在200字以内。" ), generation_config={ "temperature": 0.2, "max_output_tokens": 512, }, )我把temperature从默认值降到了0.2,输出上限设到512。这个组合特别适合客服场景:稳定优先、简洁优先。实测下来,答案一致性能提升非常明显,用户角度感受就是“这个客服不飘了”。
3.3 第三步:把会话从内存搬到持久化存储
接下来是接线到生产的第一步——会话持久化。把InMemorySessionService换成持久化实现。
# 把 InMemorySessionService 替换为你项目里接入的持久化实现 session_service = PersistentSessionService(database_url="sqlite:///sessions.db") runner = Runner( agent=agent, app_name="support_app", session_service=session_service, )这一步做完,Agent重启、横向扩容之后,用户历史仍然保留。用户id加session id的设计,也保证了不同用户之间的会话完全隔离。
3.4 第四步:多Agent协作与配置隔离
很多场景不是一个Agent能覆盖的。我做过一个旅游咨询Agent,主Agent负责对话入口,子Agent分别覆盖酒店查询、机票查询、行程规划。每个子Agent挂不同的工具,配置按各自任务做了差异化:
hotel_agent = Agent( name="hotel_agent", model="gemini-2.0-flash", instruction="你是酒店查询助手,只处理酒店相关请求,调用酒店搜索工具。", tools=[hotel_search_tool], generation_config={"temperature": 0.2}, ) flight_agent = Agent( name="flight_agent", model="gemini-2.0-flash", instruction="你是机票查询助手,只处理航班相关请求。", tools=[flight_search_tool], generation_config={"temperature": 0.2}, ) main_agent = Agent( name="travel_assistant", model="gemini-2.0-flash", instruction="你是旅游助手,负责理解用户意图并分发给对应的子Agent。", sub_agents=[hotel_agent, flight_agent], )子Agent的配置是独立的,这就是多Agent协作里很重要的“配置隔离”思想。根Agent不需要也不应该知道酒店搜索的细节,它的职责是路由和汇总。反过来,子Agent不需要也不应该继承根Agent的Prompt。这种隔离让每个Agent边界清晰,出现问题也容易定位。
3.5 完整可运行示例
最后给一个相对完整的示例,把上面的点都串在一起:
from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.artifacts import InMemoryArtifactService # 子Agent:酒店查询 hotel_agent = Agent( name="hotel_agent", model="gemini-2.0-flash", instruction="你是酒店查询助手,只处理酒店相关请求,调用酒店搜索工具。", tools=[hotel_search_tool], generation_config={"temperature": 0.2, "max_output_tokens": 512}, ) # 根Agent:旅游助手 main_agent = Agent( name="travel_assistant", model="gemini-2.0-flash", instruction="你是旅游助手,负责理解用户意图并分发给对应的子Agent。", sub_agents=[hotel_agent], generation_config={"temperature": 0.3, "max_output_tokens": 1024}, ) session_service = PersistentSessionService(database_url="sqlite:///sessions.db") runner = Runner( agent=main_agent, app_name="travel_app", session_service=session_service, ) session = session_service.create_session( app_name="travel_app", user_id="u001", session_id="s001", ) events = runner.run( user_id="u001", session_id="s001", message="帮我找一下南京夫子庙附近评分4.5以上的酒店", ) for event in events: if event.content: print(event.content)这个示例可以直接抄来改。到这一步,你手里的Agent已经具备了:稳定的生成参数、持久化的会话、清晰的Agent边界。从最小可用到能上线,RunConfig相关的核心配置基本覆盖完了。接下来如果还想继续优化,通常就该往性能、成本和可观测性方面走了。
4. 常见配置异常的完整排查链路
这一章写几个我实际踩过的坑,每个都按“现象-排查-根因-修复”的顺序来。
4.1 现象一:模型配置没生效,Agent用了奇怪的默认行为
有次我建了一个Agent,model明明设成了gemini-2.0-flash,但实际输出风格明显不是这个模型该有的表现。第一反应是SDK bug。排查链路:
- 确认Agent初始化代码里model确实传了——检查了,传了。
- 查看环境变量——发现系统里存在一个旧的GOOGLE_GENAI_MODEL环境变量,SDK的加载优先级导致环境变量覆盖了代码里的配置。
- 清掉环境变量,重启,问题消失。
这个坑的教训是:配置优先级要理清。代码参数、RunConfig、环境变量这三者有明确的覆盖关系,别想当然认为代码里的值一定生效。排查的时候先看有没有“更高优先级”的配置源存在。调试时可以打印Agent初始化后的实际配置对象,很多时候配置对象里已经反映了加载完成的最终值,确认覆盖关系很快。
4.2 现象二:temperature设了0,输出还是不稳定
另一个让我困惑的问题是,客服Agent明明把temperature设成了0,但同一问题问两次,答案还是有细微差异。排查链路:
- 先怀疑模型API是不是忽略了temperature——从日志看,请求参数里temperature确实传了0。
- 再怀疑是不是有多个generation_config在互相覆盖——检查Agent初始化,发现子Agent自己又设了一个temperature=0.7的配置,而调用链路上实际走的是子Agent。
- 把子Agent配置改一致,重新测试,输出稳定了。
核心教训:在多Agent场景里,实际生效的是你调用链路末端那个Agent的配置。根Agent的配置不会自动传给子Agent,必须各自显式设置。
4.3 现象三:Agent“失忆”,多轮对话上下文丢失
用户说第三句话时,Agent完全忘了前面两句话的内容。这是那种不报错但体验极差的问题。我的排查链路:
- 确认每次run都带了同一个session_id——检查代码,确实带了。
- 再看session_service的实现——发现用的是InMemorySessionService,服务进程有一次重启,所有内存会话都没了。
- 切换到持久化session service,并确认会话恢复逻辑正确,问题解决。
另外补充一个隐藏坑:即使session没丢,如果上下文整理策略配置太激进,比如每轮只保留最后一条消息,Agent照样会“失忆”。要把历史保留策略和模型上下文窗口对齐。
4.4 现象四:运行时卡顿与超时
最后一个是性能和超时问题。Agent逻辑不复杂,但线上经常出现单次响应超过20秒的情况。排查链路:
- 先看metrics——发现模型调用本身耗时不高,大头花在了等待工具返回上。
- 再看工具调用——发现一个外部HTTP接口,每次调用需要5秒,而Agent为了确认结果会连续调用三次。
- 优化方法分两层:一是给工具调用加缓存,相同参数直接复用上次结果;二是在运行时配置里限制Agent的最大执行轮次,减少无意义的重复调用。
- 调优之后,平均响应时间从20多秒降到了6秒左右。
这个案例说明,性能问题不一定是模型慢,有时是运行时调度和工具调用策略不合理。RunConfig里的轮次限制、超时策略都是调节杠杆。我平时会把“耗时指标”看作配置调优的导航仪,哪个环节数值异常,就去对应那一层找问题。
5. 最后聊几点我的个人体会
写了这么多,最后分享几个我实际工作中的习惯,不保证全对,但至少帮我少踩了很多坑。
第一,把配置模板化。我在项目里会维护一个config目录,按场景拆文件:support.conf、creative.conf、multi_agent.conf。每个场景都固化好model、temperature、max_output_tokens、session_service等关键值。新Agent直接套模板,不裸写参数。
第二,默认打开metrics。无论做demo还是正式项目,我都会把调用耗时、token消耗、错误类型的打点打开。出了问题先看数据,不要猜。
第三,版本化配置。RunConfig这种“怎么跑”的配置和代码一样,应该进入版本管理。常常有这种情况:线上表现突然变化,一查是某次部署把配置改了。配置入库,才好追溯。
第四,小步验证。给一个Agent改配置时,不要一次改七八个参数。一次改一个,跑一轮测试看效果,再改下一个。否则出了变化你根本不知道是哪个参数引起的。
第五,预生产环境必须有一份和线上完全一致的RunConfig。很多问题都是因为预生产和线上的配置漂移。我的做法是部署时用配置渲染模板,从同一个配置文件生成预生产和线上两份配置,人工review差异。这个习惯可以明显减少“测试环境好好的,上线就出问题”的诡异事件。
RunConfig看起来不复杂,但它决定了Agent在真实环境里的稳定性、成本和响应速度。把“怎么跑”配置清楚,你的Agent才能真正从Demo变成产品。