尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Day 2:搭建 Agent 项目骨架

Day 2:搭建 Agent 项目骨架 文章目录一、本篇目标二、先理解一个核心概念Agent 项目为什么需要工程骨架三、项目目录设计为什么 main.py 不负责所有事情为什么暂时不增加更多目录四、准备 Python 环境和依赖1. 创建虚拟环境2. 创建依赖文件3. 创建 .gitignore五、配置文件与环境变量1. 创建 .env.example2. 为什么不把配置写在 Python 常量里3. 实现统一配置对象4. 配置模块的几个设计点六、日志基础让 Agent 的运行过程可追踪日志级别怎么选七、错误处理基础定义自己的异常类型八、封装模型客户端为什么不在客户端里直接打印答案为什么使用 time.monotonic()九、实现程序入口为什么使用退出码十、补一个配置测试十一、运行效果十二、常见问题与排错1. ModuleNotFoundError: No module named app2. 修改了 .env 但程序没有生效3. 日志没有输出4. 生产环境应该把日志写到文件吗5. 为什么不在这里实现重试十三、工程化改进这个模板还可以继续演进API Key 是否应该放进配置对象什么时候应该引入配置类库十四、本篇小结十五、课后练习练习 1增加应用端口配置练习 2增加脱敏日志函数练习 3增加模型调用耗时告警阶段验收✍创作者全栈弄潮儿 个人主页全栈弄潮儿的个人主页️ 个人社区欢迎你的加入全栈开发社区 专栏AI Agent 开发实战从 0 到生产级智能体这是《AI Agent 开发实战从 0 到生产级智能体》的第 2 篇。上一篇我们理解了 Agent 的组成并写出了第一个可以运行的最小对话 Agent。代码能跑起来只是起点如果把 API Key、模型名称、日志和异常处理全部散落在main.py中后面增加工具、知识库和工作流时项目会很快变得难以维护。所以第二篇先不急着增加“更聪明”的能力而是把项目骨架搭稳配置集中管理依赖可重复安装日志可以追踪错误能够被识别和处理。本篇的最终产物是一个可以复用的 Agent 项目模板agent-workbench/ ├── app/ │ ├── __init__.py │ ├── config.py │ ├── errors.py │ ├── logging_config.py │ ├── model_client.py │ └── main.py ├── tests/ │ └── test_config.py ├── .env.example ├── .gitignore ├── requirements.txt └── README.md完成后后续的 Tool Calling、RAG 和工作流代码都可以在这个模板上继续扩展。一、本篇目标完成下面 5 件事设计适合 Agent 项目的目录结构。使用虚拟环境和依赖文件管理 Python 项目。通过环境变量配置 API Key、模型和运行参数。建立统一的日志格式和日志级别。区分配置错误、模型调用错误和未知错误。本篇的最终验收标准是[ ] 新机器可以根据依赖文件安装项目。 [ ] API Key 不出现在源码中。 [ ] 程序启动时能够读取并校验配置。 [ ] 日志包含时间、级别、模块和消息。 [ ] 配置错误与运行错误能够给出不同提示。二、先理解一个核心概念Agent 项目为什么需要工程骨架Agent 项目通常会同时依赖模型服务、外部工具、数据库和用户输入。它比一个普通脚本更容易遇到三类问题同一份代码在不同环境使用了不同配置。出错后只有一句“调用失败”无法定位具体阶段。为了快速验证功能把所有代码写在一个文件里后续无法复用。一个合格的项目骨架至少要把下面几类职责分开模块主要职责本篇是否实现配置层读取和校验环境变量是日志层统一输出运行信息是错误层定义可识别的异常类型是模型层封装大模型客户端是Agent 层管理任务和对话状态下一篇开始扩展工具层调用天气、搜索和数据库后续实现拆分的目的不是“目录看起来专业”而是让变化被限制在合理范围内。例如切换模型服务商应该主要修改配置和模型客户端而不是搜索整个项目替换字符串。三、项目目录设计我们继续使用agent-workbench作为项目名。第一版目录如下agent-workbench/ ├── app/ │ ├── __init__.py │ ├── config.py # 配置读取和校验 │ ├── errors.py # 项目级异常类型 │ ├── logging_config.py # 日志格式和输出位置 │ ├── model_client.py # 模型客户端封装 │ └── main.py # 程序入口 ├── tests/ │ └── test_config.py # 配置模块测试 ├── .env.example # 配置示例不包含真实密钥 ├── .gitignore ├── requirements.txt └── README.md为什么main.py不负责所有事情入口文件只负责启动流程读取配置 ↓ 初始化日志 ↓ 创建模型客户端 ↓ 启动 Agent如果main.py同时负责读取环境变量、拼接 Prompt、调用模型、打印日志和捕获异常后续每增加一个工具入口文件都会继续膨胀。把基础能力提前抽出来下一篇只需要关注 Agent 的业务逻辑。为什么暂时不增加更多目录当前项目还只有一个模型客户端不需要提前创建十几个空目录。目录结构应该随着真实职责增长而不是为了“看起来像大型项目”而增加抽象层。四、准备 Python 环境和依赖1. 创建虚拟环境建议 Python 3.11 或更高版本。执行mkdiragent-workbenchcdagent-workbench python-mvenv .venv激活虚拟环境# macOS / Linuxsource.venv/bin/activate# Windows PowerShell.venv\Scripts\Activate.ps1激活后终端提示符前通常会出现.venv。以后安装依赖和运行程序都要确认当前使用的是这个环境。可以用下面命令确认 Python 路径python-cimport sys; print(sys.executable)2. 创建依赖文件在项目根目录创建requirements.txtopenai1.40.0 python-dotenv1.0.1安装依赖python-mpipinstall-rrequirements.txt为什么不直接把依赖安装命令写在 README 里因为依赖文件是项目的可执行约定新成员、CI 和部署环境都可以使用同一条命令安装。安装完成后可以导出当前环境的精确版本python-mpip freezerequirements-lock.txt入门项目暂时保留requirements.txt就够了。团队项目需要根据发布流程决定是否提交锁定版本文件。3. 创建.gitignore.env .venv/ __pycache__/ *.py[cod] .pytest_cache/ logs/其中.env必须忽略。配置文件一旦被提交到公开仓库API Key 可能在很短时间内被滥用。五、配置文件与环境变量1. 创建.env.exampleAPP_ENVdev LOG_LEVELINFO OPENAI_API_KEYreplace-with-your-key OPENAI_BASE_URLhttps://api.openai.com/v1 OPENAI_MODELgpt-4o-mini MODEL_TIMEOUT_SECONDS30.env.example可以提交到仓库因为它只描述变量名称和示例值不包含真实密钥。开发者复制一份cp.env.example .env然后在.env中填入实际配置。2. 为什么不把配置写在 Python 常量里下面这种写法不适合项目API_KEYsk-real-keyMODELgpt-4o-mini它有三个问题密钥容易被提交到 Git。开发、测试和生产环境需要修改源码。配置值散落在多个模块无法快速确认当前运行参数。环境变量把“代码”和“运行环境”分开。代码描述行为环境变量提供具体配置。3. 实现统一配置对象创建app/config.pyfrom__future__importannotationsimportosfromdataclassesimportdataclassfromdotenvimportload_dotenvfrom.errorsimportConfigurationErrordataclass(frozenTrue)classSettings:app_env:strlog_level:stropenai_api_key:stropenai_base_url:str|Noneopenai_model:strmodel_timeout_seconds:floatdef_required(name:str)-str:valueos.getenv(name,).strip()ifnotvalueorvaluereplace-with-your-key:raiseConfigurationError(f缺少有效配置{name})returnvaluedef_positive_float(name:str,default:float)-float:raw_valueos.getenv(name,str(default)).strip()try:valuefloat(raw_value)exceptValueErrorasexc:raiseConfigurationError(f配置{name}必须是数字)fromexcifvalue0:raiseConfigurationError(f配置{name}必须大于 0)returnvaluedefload_settings()-Settings:读取 .env 和系统环境变量并校验必填配置。load_dotenv()log_levelos.getenv(LOG_LEVEL,INFO).upper()iflog_levelnotin{DEBUG,INFO,WARNING,ERROR,CRITICAL}:raiseConfigurationError(LOG_LEVEL 必须是 DEBUG、INFO、WARNING、ERROR 或 CRITICAL)base_urlos.getenv(OPENAI_BASE_URL,).strip()orNonereturnSettings(app_envos.getenv(APP_ENV,dev).strip()ordev,log_levellog_level,openai_api_key_required(OPENAI_API_KEY),openai_base_urlbase_url,openai_modelos.getenv(OPENAI_MODEL,gpt-4o-mini).strip(),model_timeout_seconds_positive_float(MODEL_TIMEOUT_SECONDS,30),)4. 配置模块的几个设计点Settings使用frozenTrue表示配置加载完成后不能在运行过程中被随意修改。这样可以避免某个模块悄悄改变全局模型或超时时间。load_settings()是显式函数而不是导入模块时自动执行。导入时不读取配置测试会更容易编写程序启动时也能决定如何处理配置错误。环境变量仍然可以覆盖.env中的值这是python-dotenv的常见使用方式部署平台注入的环境变量优先级更高。六、日志基础让 Agent 的运行过程可追踪模型调用失败时只打印“失败了”是不够的。至少需要知道什么时候失败 哪个模块失败 日志级别是什么 失败原因是什么创建app/logging_config.pyfrom__future__importannotationsimportloggingimportsys LOG_FORMAT(%(asctime)s | %(levelname)s | %(name)s | %(message)s)defconfigure_logging(level:str)-None:配置全局日志只允许应用初始化时调用一次。logging.basicConfig(levelgetattr(logging,level),formatLOG_FORMAT,datefmt%Y-%m-%d %H:%M:%S,streamsys.stdout,forceTrue,)defget_logger(name:str)-logging.Logger:returnlogging.getLogger(name)在业务模块中使用 logger不要到处使用print()from.logging_configimportget_logger loggerget_logger(__name__)logger.info(开始初始化模型客户端)logger.debug(当前模型%s,model_name)logger.warning(模型响应时间较长%.2f 秒,elapsed_seconds)注意DEBUG日志可以记录模型名称和耗时但不要记录 API Key、完整用户隐私内容或完整的授权请求头。日志级别怎么选级别用途DEBUG本地排查参数和调用细节INFO正常启动、节点完成和关键流程WARNING可恢复问题或性能异常ERROR当前请求失败但进程可以继续CRITICAL应用无法继续运行开发环境可以使用DEBUG生产环境通常从INFO开始根据日志量和排错需要调整。七、错误处理基础定义自己的异常类型创建app/errors.pyclassAppError(Exception):所有可预期的应用错误的基类。classConfigurationError(AppError):配置缺失或格式不正确。classModelClientError(AppError):模型客户端初始化或调用失败。classAgentRuntimeError(AppError):Agent 执行过程中的业务错误。不要在所有地方都直接抛出Exception。自定义异常让入口层能够区分错误配置错误提示用户检查.env。模型错误提示检查网络、Key、模型和服务状态。Agent 业务错误提示当前任务失败但不一定需要退出进程。八、封装模型客户端创建app/model_client.pyfrom__future__importannotationsimporttimefromtypingimportAnyfromopenaiimportOpenAIfrom.configimportSettingsfrom.errorsimportModelClientErrorfrom.logging_configimportget_logger loggerget_logger(__name__)classModelClient:def__init__(self,settings:Settings)-None:options:dict[str,Any]{api_key:settings.openai_api_key,timeout:settings.model_timeout_seconds,}ifsettings.openai_base_url:options[base_url]settings.openai_base_url self.clientOpenAI(**options)self.modelsettings.openai_modeldefchat(self,prompt:str,system:str)-str:started_attime.monotonic()logger.info(开始调用模型model%s,self.model)try:responseself.client.chat.completions.create(modelself.model,messages[{role:system,content:system},{role:user,content:prompt},],temperature0.2,)contentresponse.choices[0].message.contentifnotcontentornotcontent.strip():raiseModelClientError(模型返回了空内容)exceptModelClientError:raiseexceptExceptionasexc:raiseModelClientError(f模型调用失败{exc})fromexcfinally:elapsedtime.monotonic()-started_at logger.info(模型调用结束elapsed%.2f 秒,elapsed)returncontent.strip()这里暂时只封装最基础的一次对话调用下一篇 Tool Calling 会在这个客户端上增加工具参数和消息循环。为什么不在客户端里直接打印答案客户端的职责是“调用模型并返回结果”而不是决定结果显示在哪里。终端、Web API 和异步任务都可能复用它。如果客户端直接print()上层就无法控制输出格式。为什么使用time.monotonic()统计耗时应该使用单调时钟它不受系统时间被手动调整或网络同步影响。日志里的耗时只用于观察调用性能不用于展示真实日期。九、实现程序入口创建app/main.pyfrom__future__importannotationsfrom.configimportload_settingsfrom.errorsimportAppError,ConfigurationErrorfrom.logging_configimportconfigure_logging,get_loggerfrom.model_clientimportModelClient loggerget_logger(__name__)defmain()-int:try:settingsload_settings()configure_logging(settings.log_level)logger.info(Agent 项目启动env%s,settings.app_env)model_clientModelClient(settings)answermodel_client.chat(prompt请用一句话解释 Agent 项目的配置管理为什么重要。,system你是一个简洁的技术助理。,)print(fAgent{answer})logger.info(Agent 项目运行完成)return0exceptConfigurationErrorasexc:print(f配置错误{exc})return2exceptAppErrorasexc:logger.exception(应用错误)print(f运行失败{exc})return1exceptException:logger.exception(未处理的未知错误)print(运行失败发生未知错误请查看日志)return1if__name____main__:raiseSystemExit(main())项目根目录需要创建空文件app/__init__.py这样可以使用模块方式启动python-mapp.main为什么使用退出码命令行程序除了输出文字还应该通过退出码告诉 Shell、CI 或部署平台运行是否成功0运行成功。1运行时失败。2配置错误。后续接入自动部署或定时任务时退出码可以帮助系统判断是否需要告警。十、补一个配置测试为了验证配置校验确实有效创建tests/test_config.pyimportpytestfromapp.configimportload_settingsfromapp.errorsimportConfigurationErrordeftest_missing_api_key(monkeypatch):# 显式设置为空避免本机 .env 中已有密钥影响测试结果。monkeypatch.setenv(OPENAI_API_KEY,)monkeypatch.setenv(OPENAI_MODEL,test-model)withpytest.raises(ConfigurationError,matchOPENAI_API_KEY):load_settings()deftest_invalid_log_level(monkeypatch):monkeypatch.setenv(OPENAI_API_KEY,test-key)monkeypatch.setenv(LOG_LEVEL,VERBOSE)withpytest.raises(ConfigurationError,matchLOG_LEVEL):load_settings()需要把测试依赖加入requirements.txtopenai1.40.0 python-dotenv1.0.1 pytest8.0.0运行测试python-mpytest测试不需要真实 API Key也不会调用模型。配置模块应该尽量保持可独立测试这也是为什么我们没有在导入config.py时自动创建客户端。十一、运行效果正确配置.env后运行python-mapp.main可能看到2026-09-07 10:20:12 | INFO | app.main | Agent 项目启动envdev 2026-09-07 10:20:12 | INFO | app.model_client | 开始调用模型modelgpt-4o-mini 2026-09-07 10:20:13 | INFO | app.model_client | 模型调用结束elapsed0.86 秒 Agent配置集中管理可以避免密钥泄露并让不同环境使用不同运行参数。 2026-09-07 10:20:13 | INFO | app.main | Agent 项目运行完成删除.env中的 API Key再次运行配置错误缺少有效配置OPENAI_API_KEY这两种结果应该明显不同前者是正常运行日志后者是启动前的配置错误。错误信息越明确排查成本越低。十二、常见问题与排错1.ModuleNotFoundError: No module named app确认你在项目根目录运行python-mapp.main不要先进入app/目录再执行python main.py这样相对导入可能失效。2. 修改了.env但程序没有生效检查以下内容.env是否位于项目根目录。是否使用了正确的变量名。当前 Shell 是否已经设置了同名环境变量。是否重启了程序。如果系统环境变量已经存在通常会覆盖.env中的同名值。可以先执行python-cimport os; print(os.getenv(OPENAI_MODEL))不要打印OPENAI_API_KEY的值来排查避免密钥进入终端历史或日志。3. 日志没有输出configure_logging()必须在第一次写日志前调用。本文在加载配置后立即初始化日志因此启动阶段的配置错误会使用普通print()输出初始化成功后才使用结构化日志。4. 生产环境应该把日志写到文件吗本篇先输出到标准输出方便本地开发和容器采集。部署到服务器后可以交给进程管理器、容器平台或日志系统统一收集。不要在每个模块里各自创建 FileHandler否则容易出现重复日志和文件句柄管理问题。5. 为什么不在这里实现重试重试策略依赖错误类型。认证失败不应该重试限流和网络超时可能需要指数退避业务参数错误则应该直接返回。下一篇进入 Tool Calling 后会结合工具调用场景设计有限重试。十三、工程化改进这个模板还可以继续演进当前模板已经能支撑后续开发但距离生产环境仍有差距方向当前实现后续改进配置.env 环境变量密钥管理服务和分环境配置依赖requirements.txt锁定版本和自动化构建日志标准输出JSON 日志、Trace ID 和集中采集错误项目级异常错误码、用户提示和告警分级模型单次调用超时、限流、重试和成本统计测试配置单测模型 Mock、工具测试和集成测试入口命令行Web API、队列任务和定时任务API Key 是否应该放进配置对象在本篇为了让模型客户端完成初始化Settings中暂时保存了 API Key。更严格的生产实现可以让密钥只在客户端构造时读取避免被日志、调试输出或状态序列化意外带出。无论采用哪种方式都必须遵守一个原则密钥不能进入日志、Prompt、消息历史和用户可见响应。什么时候应该引入配置类库当配置字段超过十几个或者需要复杂类型校验、嵌套配置和多环境覆盖时可以考虑 Pydantic Settings 等方案。当前字段数量较少标准库dataclass已经足够直观。十四、本篇小结今天我们没有增加新的 Agent 智能能力而是把后续开发需要的基础设施搭好了目录职责分离 ↓ 虚拟环境和依赖文件 ↓ 环境变量和配置校验 ↓ 统一日志 ↓ 项目级错误处理 ↓ 可复用的模型客户端请记住这三个结论Agent 项目先要能稳定运行再逐步增加工具和复杂工作流。配置、日志和错误处理不是上线前才补的“杂事”而是从第一天就应该存在的骨架。每个模块都要有明确职责入口文件只负责组织流程不负责承载所有业务逻辑。下一篇我们会在这个项目模板上接入第一个真实工具学习 Tool Calling 如何定义工具、校验参数并处理调用结果。十五、课后练习请在现有项目上完成下面练习练习 1增加应用端口配置新增APP_PORT环境变量并在Settings中校验它必须是1到65535之间的整数。练习 2增加脱敏日志函数实现mask_secret(value)只保留密钥前 4 位和后 2 位中间全部替换成*。测试日志中不会输出完整 API Key。练习 3增加模型调用耗时告警新增MODEL_SLOW_THRESHOLD_SECONDS配置。当一次模型调用超过阈值时输出WARNING日志但不要自动重试。阶段验收当你可以在不修改main.py业务流程的情况下新增一个配置项、一个日志字段和一个异常类型时就完成了本篇的目标。下一篇开始进入 Prompt、上下文与消息结构。✍坚持原创求关注点赞收藏
返回列表