十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 8 篇】

【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 8 篇】 配置系统优先级链YAML、.env 与 Profile 的三层隔离导读切换模型还要改代码API key 和配置混在一个文件里同一个人需要两套 Agent 人设Hermes 配置系统用三层隔离解决这三个问题。本文拆解 41716 字节的 s11_configuration_system.py讲清楚深度合并、环境变量展开与 Profile 切换的完整实现。三个痛点为什么需要配置系统先问三个问题。第一个从 OpenRouter 切到 Anthropic你要改几行代码如果模型名、base_url 散落在 10 个文件里改一次就是一次灾难。第二个API key 放在 YAML 里然后提交到 Git 仓库。等收到泄露告警邮件已经晚了。第三个白天你是写代码的 coder晚上你是写文章的 writer。两个人设、两套记忆、两套工具集——难道要维护两个项目这三个问题就是配置系统要解决的。核心目标把Agent 怎么运行从代码里抽出来变成可声明、可合并、可隔离的外部配置。DEFAULT_CONFIG代码里的完整默认字典先看默认配置长什么样。这是s11_configuration_system.py里的真实代码DEFAULT_CONFIG{model:anthropic/claude-sonnet-4,base_url:https://openrouter.ai/api/v1,api_key:,fallback:{model:,base_url:,api_key:},limits:{max_iterations:30,max_child_iterations:15,max_retries:3,max_continuations:3,},compression:{threshold:50000,protect_first:3,keep_recent_tool_results:3,tail_token_budget:20000,},memory:{memory_char_limit:2200,user_char_limit:1375},db_path:state.db,}这个字典有两个作用提供默认值 定义 schema。注意几个数字——它们不是随便写的是前面章节的呼应max_iterations: 30对应 s01 的主循环上限max_child_iterations: 15对应 s10 的子 Agent 深度限制compression.threshold: 50000对应 s05 的上下文压缩触发阈值memory.memory_char_limit: 2200对应 s07 的记忆窗口大小配置系统不是凭空造的它把前面所有机制的参数统一收编了。_deep_merge为什么不用 dict.update()配置系统最核心的函数。def_deep_merge(base:dict,override:dict)-dict:Recursively merge two dicts. Override values take precedence.resultbase.copy()forkey,valueinoverride.items():if(keyinresultandisinstance(result[key],dict)andisinstance(value,dict)):result[key]_deep_merge(result[key],value)else:result[key]valuereturnresult逻辑很直白递归合并两个字典override 的值优先。但为什么不用dict.update()看这个场景用户只想改compression.thresholdYAML 里写了compression:threshold:0.65如果用dict.update()compression整个子字典会被覆盖——protect_first、keep_recent_tool_results、tail_token_budget全丢了。_deep_merge递归进入子字典只覆盖声明了的字段。用户配了什么就只改什么。load_config解析失败也不阻塞启动defload_config(config_path:Path|NoneNone)-dict:ifconfig_pathisNone:config_pathHERMES_HOME/config.yamlifnotconfig_path.exists():return_expand_env_vars(DEFAULT_CONFIG.copy())try:raw_textconfig_path.read_text(encodingutf-8)user_configyaml.safe_load(raw_text)or{}exceptException:user_config{}# YAML 解析异常就退回默认值merged_deep_merge(DEFAULT_CONFIG,user_config)return_expand_env_vars(merged)注意三个细节第一DEFAULT_CONFIG.copy()——浅拷贝。如果不 copy同进程二次 load 会读到被污染的值。这是一个经典的 Python 坑。第二except Exception: user_config {}。YAML 写坏了退回默认值。坏配置不阻塞启动。第三yaml.safe_load(raw_text) or {}——空文件返回Noneor {}兜底。load_env手写 .env 解析不依赖 python-dotenv手写一个简单解析器defload_env(env_path:Path|NoneNone):ifenv_pathisNone:env_pathHERMES_HOME/.envifnotenv_path.exists():returnforlineinenv_path.read_text(encodingutf-8).splitlines():lineline.strip()ifnotlineorline.startswith(#):continueifinline:key,_,valueline.partition()keykey.strip()valuevalue.strip().strip().strip()os.environ.setdefault(key,value)两个关键点。第一setdefault语义真实环境变量优先.env只做缺省值。这意味着你可以在 shell 里export OPENAI_API_KEYxxx.env里的值不会覆盖它。第二手写解析不引入额外依赖。一个 20 行的函数解决 80% 的需求。_expand_env_vars${VAR} 展开config.yaml 里可以写api_key:${OPENAI_API_KEY}运行时展开def_expand_env_vars(value):ifisinstance(value,str):defreplacer(match):var_namematch.group(1)returnos.getenv(var_name,match.group(0))returnre.sub(r\$\{(\w)\},replacer,value)elifisinstance(value,dict):return{key:_expand_env_vars(val)forkey,valinvalue.items()}elifisinstance(value,list):return[_expand_env_vars(item)foriteminvalue]returnvalue递归处理字符串、字典、列表。关键在replacer里的match.group(0)——变量不存在时保留原${VAR}不静默变成空串。这样调用方就知道这个值没配好而不是拿到一个空字符串去请求 API然后收到一个莫名其妙的 401。优先级链谁覆盖谁整个配置系统的核心规则一句话命令行参数 环境变量 config.yaml 默认值DEFAULT_CONFIG从下往上读默认值是最底层兜底config.yaml 覆盖默认值环境变量再往上盖一层命令行参数最高优先级。这个设计的好处每个环境只需要声明自己不同的部分。开发环境用默认值测试环境用 config.yaml 覆盖几个字段生产环境再用环境变量注入密钥。两文件分离config.yaml vs .envHermes 的一个独特设计把配置拆成两个文件。config.yaml结构化行为配置。模型、限制、压缩阈值、记忆窗口——这些可以进版本控制可以团队共享。.env秘密信息。API key、token——0600 权限、.gitignore、每人各自一份。为什么要拆因为秘密信息和结构化配置的生命周期完全不同。config.yaml 要 review、要版本化、要团队讨论.env 要保密、要隔离、要每人不同。混在一起要么泄露密钥要么无法共享。Profile 隔离切换目录就是切换世界同一个人需要两套 Agent 人设怎么办看这段代码HERMES_HOMEPath(os.getenv(HERMES_HOME,Path.home()/.hermes))load_env()_configload_config()HERMES_HOME环境变量指向不同目录~/.hermes/profiles/coder/→ coder 人设~/.hermes/profiles/writer/→ writer 人设不需要任何条件分支。目录换了整个世界就换了。每个 Profile 有自己独立的 config.yaml、.env、state.db、记忆文件。模型、人设、工具集、记忆——全部隔离。这就是配置系统的终极形态配置不是参数是环境。启动接入核心循环不知道配置来自哪里最后看整体流程启动入口CLI/Gateway → load_env() → load_config() deep_merge → _expand_env_vars() → 构建 AIAgent 参数 → 核心循环运行核心循环不直接读 config.yaml只接收参数。这意味着什么核心循环可以被任何入口复用——CLI、API 服务、测试脚本——配置来源可以随时替换。今天用 YAML明天换成远程配置中心核心循环一行不用改。这就是依赖注入。配置版本迁移教学版的边界生产级配置系统还需要处理字段改名、迁移。真实仓库文档描述了ENV_VARS_BY_VERSION_normalize_max_turns_config的完整方案。教学版的范围更克制深度合并用默认值补齐缺失字段字段改名/移动需要显式迁移函数_config_version字段追踪版本不要过度设计。教学版的目标是讲清楚核心机制迁移系统点到为止。初学者 5 错最后总结最常见的五个坑。1. API key 写进 config.yaml应该在 .env 里用${VAR}引用。YAML 会进 Git 仓库key 会泄露。2. 直接修改 DEFAULT_CONFIGload_config必须copy.deepcopy。否则同进程二次 load读到的是被污染的值。3. 用 dict.update() 代替深度合并嵌套字段会丢。用户只配了一个字段其他全没了。4. Profile 之间共享 .env切换 Profile 用错 key高权限 key 暴露给低权限场景。每个 Profile 必须有独立的 .env。5. 忘记配置迁移字段改名/移动需要显式迁移函数。不迁移旧配置静默失效行为不可预期。小结与下篇预告配置系统是阶段 2 的收官。s07-s11从记忆、技能到安全、委派所有机制的参数现在都被统一收编进三层配置体系。配置系统的本质把变化从代码里赶出去。代码只负责逻辑变化交给配置。切换模型不改代码注入密钥不进仓库切换人设不换项目。下一篇第 9 篇Gateway 与平台适配器——阶段 3 开始让 Agent 接入真实世界。你在自己的项目里配置系统是怎么设计的遇到过哪些坑欢迎在评论区聊聊。参考文献Hermes Agent 教学仓库agents/s11_configuration_system.py本文代码素材41716 字节真实可运行Hermes Agent 教学仓库docs/zh/s11-configuration-system.md两文件分离、Profile、迁移系统详解源码获取如需本系列全部源码请在以下链接克隆https://gitcode.com/ganxin7932508/learn-hermes-agent.git
返回列表