第六课环境变量、配置系统、日志系统与最小 Agent 主循环这节课你会学什么前面几课你已经学了这些地基Python 基础asyncioHTTP / REST / WebSocketJSON SchemaTool Router这一课开始我们把这些东西真正拼起来做出一个能持续执行任务的最小 Agent 外壳。学完这一课你应该能理解这些概念什么是环境变量为什么 API Key 不应该写死在代码里什么是配置系统.env、os.environ开发环境和生产环境的区别为什么 Agent 从第一天就要有日志日志级别结构化日志的基本思想request id / task id最小 Agent 主循环是什么Agent 为什么不是“一次函数调用”而是“循环决策系统”最后你会做一个小练习写一个带配置、日志、工具调用和主循环的最小 Agent。1. 为什么这节课很重要很多人做 Agent 时一开始只关注模型怎么调工具怎么接prompt 怎么写但真正让系统“能长期运行”的往往不是这些而是下面这些基础设施配置日志状态环境变量主循环结构如果没有这些你很快会遇到问题API Key 写死在代码里不安全改一个模型名要改很多地方出错了不知道哪一步错了同一个任务跑到一半状态丢了工具调用完流程接不起来所以这一课不是“配角”而是把前面几课变成一个真正能跑的 Agent 程序的关键一课。2. 什么是环境变量环境变量environment variable可以理解成操作系统提供给程序的一组外部配置值。例如API Key数据库地址当前运行模式日志级别默认模型名环境变量的好处是不用把敏感信息写进代码不同环境可以用不同配置改配置时不用改源码3. 为什么 API Key 不能写死在代码里错误写法API_KEYsk-abc123这样做的问题密钥容易泄漏提交到 Git 后很危险切换环境很麻烦团队协作时更容易出问题正确思路是API Key 放在环境变量里程序运行时读取它例如importos api_keyos.environ.get(OPENAI_API_KEY)print(api_key)这里的os.environ可以理解成程序读取操作系统环境变量的入口。4..env是什么.env是一种常见的配置文件用来在开发环境中保存环境变量。例如OPENAI_API_KEYyour-api-key MODEL_NAMEgpt-5 APP_ENVdev LOG_LEVELINFO然后程序启动时读取它。注意.env适合本地开发真实生产环境不一定直接依赖.env.env一般不要提交到公开仓库通常会配合.gitignore一起使用.env5. 用 Python 读取环境变量最基础的读取方式importos model_nameos.environ.get(MODEL_NAME)print(model_name)如果变量不存在get(...)会返回None。5.1 给默认值importos model_nameos.environ.get(MODEL_NAME,gpt-5)print(model_name)这里表示如果环境变量里有MODEL_NAME就用它否则默认用gpt-55.2 必填环境变量importos api_keyos.environ.get(OPENAI_API_KEY)ifnotapi_key:raiseRuntimeError(OPENAI_API_KEY is not set)适合API Key数据库密码必须提供的服务地址6. 用python-dotenv读取.env安装pipinstallpython-dotenv创建.env文件OPENAI_API_KEYdemo-key MODEL_NAMEgpt-5 APP_ENVdev LOG_LEVELINFO代码importosfromdotenvimportload_dotenv load_dotenv()api_keyos.environ.get(OPENAI_API_KEY)model_nameos.environ.get(MODEL_NAME,gpt-5)print(api_key)print(model_name)解释load_dotenv()表示把.env文件里的变量加载进当前进程环境这样你在本地开发时就很方便。7. 什么是配置系统配置系统configuration system可以理解成程序用来统一管理“可变参数”的一套机制。例如这些都属于配置模型名最大循环步数日志级别工具列表超时时间工作目录API 地址没有配置系统时你的代码可能长这样MODELgpt-5MAX_STEPS5LOG_LEVELINFO代码越来越多后这样会变乱。更好的做法是用配置对象统一管理从环境变量 / JSON /.env读取让主程序只依赖配置对象8. 用dataclass表示配置fromdataclassesimportdataclassdataclassclassAppConfig:model_name:strmax_steps:intlog_level:strapp_env:str这样配置就从“散落的变量”变成了“一个结构清晰的对象”。使用configAppConfig(model_namegpt-5,max_steps5,log_levelINFO,app_envdev,)print(config)9. 从环境变量构造配置对象importosfromdataclassesimportdataclassfromdotenvimportload_dotenv load_dotenv()dataclassclassAppConfig:model_name:strmax_steps:intlog_level:strapp_env:strdefload_config()-AppConfig:returnAppConfig(model_nameos.environ.get(MODEL_NAME,gpt-5),max_stepsint(os.environ.get(MAX_STEPS,5)),log_levelos.environ.get(LOG_LEVEL,INFO),app_envos.environ.get(APP_ENV,dev),)configload_config()print(config)注意int(os.environ.get(MAX_STEPS,5))这里必须转成整数因为环境变量读出来默认是字符串。10. dev / test / prod 是什么很多项目会区分环境dev开发环境developmenttest测试环境testingprod生产环境production为什么要区分因为不同环境常常有不同配置开发环境日志更详细测试环境使用假数据生产环境使用真实密钥和正式服务例如ifconfig.app_envdev:print(当前是开发环境)第一阶段你不需要把这套做得很复杂但至少要理解程序在不同环境下配置可能不同。11. 为什么日志系统从第一天就要有如果你写的是一次性脚本可能print()还能凑合。但 Agent 是一个会多步执行调模型调工具保存状态出现错误可能并发运行的系统。所以它必须有日志。没有日志时你会遇到为什么它没调用工具为什么它停在第 3 步为什么同一个输入今天成功、明天失败为什么 API 超时了为什么工具参数不对所以日志不是锦上添花而是 Agent 调试和运维的基础。12. 什么是日志级别常见日志级别DEBUGINFOWARNINGERROR可以这样理解DEBUG非常细的调试信息INFO正常运行信息WARNING不致命但值得注意ERROR发生了错误例如importlogging logging.basicConfig(levellogging.INFO)logging.debug(debug message)logging.info(info message)logging.warning(warning message)logging.error(error message)如果日志级别是INFO那么DEBUG不会显示INFO及以上会显示13. 最小日志系统importlogging logging.basicConfig(levellogging.INFO,format%(asctime)s | %(levelname)s | %(message)s,)loggerlogging.getLogger(agent)logger.info(Agent started)logger.warning(Tool response is slow)logger.error(Task failed)解释basicConfig(...)设置日志基础配置levellogging.INFO最低显示 INFO 级别format...定义日志显示格式getLogger(agent)创建一个具名 logger14. 同时写入终端和文件importloggingfrompathlibimportPath log_dirPath(logs)log_dir.mkdir(exist_okTrue)logging.basicConfig(levellogging.INFO,format%(asctime)s | %(levelname)s | %(message)s,handlers[logging.FileHandler(log_dir/app.log,encodingutf-8),logging.StreamHandler(),],)loggerlogging.getLogger(agent)logger.info(Agent started)解释FileHandler把日志写入文件StreamHandler把日志输出到终端logs/app.log就是最终日志文件这会让你既能实时看日志又能事后追溯。15. 什么是 structured loggingstructured logging结构化日志的意思是日志不只是随便拼一句话而是尽量带上明确字段。普通日志tool failed更好的结构化思路task_id123 tool_namesearch statusfailed errortimeout在最小阶段你不一定要上专门的 JSON 日志系统但你要开始形成习惯日志里带 task id带 tool name带 step带错误信息这样以后查问题会轻松很多。16. request id / task id 是什么16.1 request idrequest id表示一次请求的唯一编号。适合 Web API 场景一个前端请求进来生成一个 request id整条处理链路都带上这个 id16.2 task idtask id表示一次任务的唯一编号。适合 Agent 场景一个用户任务一个任务执行过程一系列工具调用最终结果和日志都关联这个 id最小版本可以这样生成importuuid task_idstr(uuid.uuid4())print(task_id)这里uuid是一种常见的唯一标识符生成方式。17. 什么是最小 Agent 主循环这是这一课最重要的概念之一。很多初学者会误以为 Agent 就是responsecall_model(prompt)print(response)但真正的 Agent 更像一个循环读取当前任务决定下一步做什么如果需要调用工具更新状态判断是否结束否则继续下一轮这就是Agent 主循环agent loop18. 为什么 Agent 是循环而不是一次调用因为很多任务不是一句话就能做完。例如帮我查一下最近 3 篇关于 AI agent 的论文并总结它们的共同点这个任务通常要分成几步先理解任务决定调用搜索工具搜索结果回来选择几篇论文提取关键信息最后总结所以 Agent 往往不是一次推理而是多轮决策 工具调用 状态更新19. 一个最小 Agent 主循环长什么样下面是一个概念版step0max_steps5whilestepmax_steps:# 1. 思考下一步# 2. 如果需要调用工具# 3. 更新状态# 4. 判断是否完成step1这还很抽象但它已经表达了核心有步数有状态有中间过程可以终止20. 定义最小状态对象fromdataclassesimportdataclass,fielddataclassclassAgentState:task_id:strtask:strstep:int0max_steps:int5finished:boolFalsehistory:list[str]field(default_factorylist)这个状态对象里有task_id当前任务编号task任务内容step当前第几步max_steps最大执行步数finished是否完成history执行历史这就是“最小状态容器”。21. 配置、日志、主循环三者的关系你可以把它们理解成配置决定系统怎么运行日志记录系统怎么运行主循环真正推动系统运行所以一个最小 Agent 常常就是加载配置 ↓ 初始化日志 ↓ 创建状态 ↓ 进入主循环 ↓ 调用模型 / 调工具 ↓ 记录日志 ↓ 保存结果22. 最小版本的“思考函数”为了先把结构搭起来我们不急着接真实模型先写一个假的“思考函数”。asyncdefthink_next_step(task:str,step:int)-str:ifstep0:returncall_tool:echoreturnfinish它的意思是第 0 步时决定调用echo工具后面就结束这只是模拟但足够帮助你看清 Agent 主循环的结构。23. 最小版本的工具函数defecho_tool(args:dict)-str:returnargs[text]这和前一课的 Tool Router 会自然接起来。24. 最小版本的 Tool Routerdefroute_tool_call(tool_name:str,arguments:dict)-dict:iftool_nameecho:try:resultecho_tool(arguments)return{ok:True,tool_name:tool_name,data:result,}exceptExceptionase:return{ok:False,tool_name:tool_name,error:str(e),}return{ok:False,tool_name:tool_name,error:unknown tool,}这还是最小版本但已经够用来搭循环。25. 小项目带配置、日志和主循环的最小 Agent现在把这一课串起来。25.1.envMODEL_NAMEgpt-5 MAX_STEPS3 LOG_LEVELINFO APP_ENVdev25.2 文件minimal_agent.pyimportasyncioimportloggingimportosimportuuidfromdataclassesimportdataclass,fieldfrompathlibimportPathfromdotenvimportload_dotenv load_dotenv()# 把 .env 文件中的变量加载到环境变量里dataclassclassAppConfig: 应用配置对象。 用来统一管理系统运行参数。 model_name:strmax_steps:intlog_level:strapp_env:strdataclassclassAgentState: Agent 状态对象。 它保存当前任务执行过程中的最小状态。 task_id:strtask:strstep:int0max_steps:int5finished:boolFalsehistory:list[str]field(default_factorylist)defload_config()-AppConfig: 从环境变量中读取配置并转换成 AppConfig 对象。 returnAppConfig(model_nameos.environ.get(MODEL_NAME,gpt-5),max_stepsint(os.environ.get(MAX_STEPS,5)),log_levelos.environ.get(LOG_LEVEL,INFO),app_envos.environ.get(APP_ENV,dev),)defsetup_logger(log_level:str)-logging.Logger: 初始化日志系统同时输出到终端和文件。 log_dirPath(logs)log_dir.mkdir(exist_okTrue)levelgetattr(logging,log_level.upper(),logging.INFO)logging.basicConfig(levellevel,format%(asctime)s | %(levelname)s | %(message)s,handlers[logging.FileHandler(log_dir/agent.log,encodingutf-8),logging.StreamHandler(),],)returnlogging.getLogger(minimal-agent)defecho_tool(args:dict)-str: 最小示例工具原样返回输入文本。 returnargs[text]defroute_tool_call(tool_name:str,arguments:dict)-dict: 最小 Tool Router。 负责根据工具名分发调用工具并统一包装返回值。 iftool_nameecho:try:resultecho_tool(arguments)return{ok:True,tool_name:tool_name,data:result,}exceptExceptionase:return{ok:False,tool_name:tool_name,error:str(e),}return{ok:False,tool_name:tool_name,error:funknown tool:{tool_name},}asyncdefthink_next_step(state:AgentState,config:AppConfig)-dict: 模拟 Agent 的“下一步决策”。 真实系统里这里通常会 - 调模型 - 根据状态决定下一步 - 输出 action awaitasyncio.sleep(0.3)ifstate.step0:return{action:call_tool,tool_name:echo,arguments:{text:f任务回显:{state.task}}}return{action:finish}asyncdefrun_agent(task:str,config:AppConfig,logger:logging.Logger)-AgentState: 最小 Agent 主循环。 执行流程 1. 创建状态 2. 进入循环 3. 每一步先思考 4. 决定是否调用工具 5. 更新状态 6. 判断是否结束 stateAgentState(task_idstr(uuid.uuid4()),tasktask,max_stepsconfig.max_steps,)logger.info(Task started | task_id%s | task%s,state.task_id,state.task)whilestate.stepstate.max_stepsandnotstate.finished:logger.info(Loop step start | task_id%s | step%s/%s,state.task_id,state.step1,state.max_steps,)decisionawaitthink_next_step(state,config)logger.info(Decision made | task_id%s | decision%s,state.task_id,decision,)ifdecision[action]call_tool:resultroute_tool_call(decision[tool_name],decision[arguments],)logger.info(Tool result | task_id%s | tool%s | result%s,state.task_id,decision[tool_name],result,)state.history.append(str(result))elifdecision[action]finish:logger.info(Task finished | task_id%s,state.task_id)state.finishedTruebreakstate.step1ifnotstate.finished:logger.warning(Task ended by max_steps | task_id%s | step%s,state.task_id,state.step,)returnstateasyncdefmain(): 程序入口。 configload_config()loggersetup_logger(config.log_level)logger.info(App started | env%s | model%s | max_steps%s,config.app_env,config.model_name,config.max_steps,)final_stateawaitrun_agent(task请回显这条任务并在下一步结束,configconfig,loggerlogger,)print(任务完成)print(task_id:,final_state.task_id)print(history:,final_state.history)print(finished:,final_state.finished)if__name____main__:asyncio.run(main())26. 这段代码里最值得看懂的点26.1load_config()它把环境变量变成一个结构化配置对象而不是让配置散落在代码各处。26.2setup_logger()它负责创建日志目录设置日志级别同时输出到终端和文件26.3AgentState它是最小状态容器后面你可以继续扩展messagestool_callsmemoryartifacts26.4think_next_step(...)这是最小“决策层”。现在是假的后面可以替换为真实模型调用。26.5run_agent(...)这就是最关键的 Agent 主循环。它体现了一个真实 Agent 的雏形有状态有步骤有决策有工具调用有日志有结束条件27. 这节课你应该怎么练第一次练习先原样跑通观察终端日志logs/agent.log最终打印的history第二次练习把think_next_step(...)改成第 0 步调用echo第 1 步再调用一次echo第 2 步再结束第三次练习给route_tool_call(...)增加一个add工具。第四次练习把history改成更结构化的形式例如history:list[dict]每步存stepactiontool_nameresult第五次练习把task_id写进最终输出文件例如outputs/{task_id}.json28. 这节课你真正应该记住的东西环境变量适合放 API Key 和环境配置。.env适合本地开发阶段。配置系统的目标是把“可变参数”集中管理。Agent 从第一天就应该有日志。INFO、WARNING、ERROR至少要会用。task_id和日志结合后排查问题会轻松很多。真正的 Agent 不是“一次模型调用”而是“循环式决策系统”。最小 Agent 主循环通常包含状态决策工具调用日志结束条件