
1. 项目概述为什么一个“个人AI助手”要从“初始化”和“Model I/O”开始“Agent实践7 - 个人AI助手 - 阶段1 项目初始化 基础Model I/O”——这个标题里藏着三个关键信号它不是在教你怎么调用一个现成的聊天框而是在搭建一个可演进、可调试、可嵌入真实工作流的AI代理骨架它强调“阶段1”说明后续还有记忆、工具调用、多步规划等层层递进的模块它把“项目初始化”和“基础Model I/O”并列放在第一阶段恰恰暴露了行业里最常被跳过的致命环节很多人一上来就猛写prompt、堆插件、接API结果两周后发现日志没法查、模型响应格式总崩、换个小模型就全链路报错。我带过不少刚接触Agent开发的某高校学生和某公司转岗工程师他们踩的第一个坑几乎全是这里没有初始化规范就没有可维护性没有Model I/O抽象就没有可替换性。所谓“个人AI助手”核心不是“AI有多聪明”而是“你能不能在30秒内让它理解你今天想干啥”。比如你输入“把上周五会议记录里提到的三个待办事项按紧急程度排序发邮件给张工”这句话背后需要拆解意图、定位文件、提取结构化数据、调用邮箱API、生成自然语言摘要——而所有这些能力都必须建立在一个稳定、透明、可观测的输入输出通道之上。这个通道就是Model I/O。它不是简单的model.chat(messages)调用而是包含请求构造、参数控制、响应解析、错误归因、耗时监控、token用量统计的一整套契约。我在某跨平台系统中实测过当I/O层缺乏统一抽象时光是处理不同模型对system角色的支持差异有的认、有的不认、有的只在首条消息生效就让团队多花了17小时排查和打补丁。所以这个阶段的本质是定义“人与AI之间第一句对话的协议”。它决定了你后续加记忆、加工具、加反思时底层是否稳如磐石。适合谁适合所有想摆脱“调API式开发”、真正构建可控AI工作流的人——无论你是独立开发者想搭自己的知识助理还是某实验室成员在做可解释性Agent研究甚至是你只是想搞懂大模型到底怎么“听懂人话”的技术爱好者。它不假设你懂LangChain或LlamaIndex但要求你愿意花2小时把输入怎么来、输出怎么走、中间哪一步可能断全都摊开来看。2. 整体设计思路为什么不用现成框架而选择“手搓”初始化与I/O层2.1 框架依赖的隐性成本便利性背后的失控风险市面上已有不少Agent框架比如某知名开源库提供了开箱即用的ReActAgent、OpenAIAgent等类。但我在某图像处理Demo的落地过程中发现直接继承这类高级封装会在三个关键点上埋下隐患调试黑盒化当模型返回格式错乱比如本该是JSON却多了个中文句号你得一层层扒源码才能定位是prompt模板问题、还是响应解析器正则写错了、抑或是模型本身输出不稳定。而框架为了通用性往往把这三者耦合在同一个方法里。参数不可见temperature0.3这种参数框架通常藏在AgentConfig对象深处你改了它却不知道它最终有没有传给底层模型客户端。更麻烦的是不同模型对同一参数的敏感度差异极大——GPT-4对top_p极不敏感而某国产大模型在top_p0.95时就开始胡言乱语如果你没在I/O层显式暴露并校验参数这种差异会直接导致行为漂移。可观测性缺失框架默认不记录原始请求体、原始响应体、实际耗时、token计数。而我在某公司项目中遇到的真实问题是业务方反馈“助手响应慢”运维查监控显示API延迟200ms最后发现是框架在后台偷偷做了3次重试1次fallback模型切换总耗时2.3秒但日志里只记了最后一次成功调用。没有初始化时就设计好的日志钩子这种问题根本无从追溯。因此本阶段选择“手搓”而非“套壳”核心逻辑是用短期多写50行代码换取长期少踩80%的线上故障。这不是反对框架而是主张“先理解契约再使用封装”。2.2 初始化设计的四大支柱环境、配置、日志、客户端一个健壮的初始化绝不是pip install完就import。它必须覆盖四个不可妥协的维度环境隔离必须强制检查Python版本≥3.9、关键依赖版本如openai1.0.0,2.0.0并拒绝在sys.platform win32且未启用WSL的环境下启动——因为某些向量库在原生Windows下存在内存泄漏。我见过某导师的课题组因忽略这点在批量处理PDF时每天凌晨自动崩溃。配置分层采用三级配置体系config/base.py硬编码默认值如DEFAULT_MODEL_NAME gpt-4-turboconfig/local.py本地开发用关闭所有远程服务、启用mock模型config/prod.py生产部署用强制开启token限流、启用Prometheus指标上报。所有配置项必须通过pydantic.BaseModel校验类型和范围比如max_tokens: int Field(gt1, le4096)避免运行时因字符串误传为整数而崩。日志契约初始化时必须注册统一日志处理器规定每条I/O日志必须包含5个固定字段[request_id]UUIDv4、[model_name]、[input_tokens]、[output_tokens]、[latency_ms]。我坚持用structlog而非logging因为它天然支持结构化输出后续接入ELK或Datadog时无需额外解析。客户端抽象不直接实例化OpenAI()而是定义BaseLLMClient接口包含async def generate(self, messages: List[Dict], **kwargs) - Dict。这样后续接入某国产大模型时只需新增QwenClient(BaseLLMClient)完全不影响上层Agent逻辑。这个抽象层就是未来替换模型的唯一入口。提示不要在初始化里做任何“连接测试”。很多教程教你在__init__.py里调用一次client.models.list()验证API Key这是反模式——它让初始化变成一个可能失败的IO操作破坏了“配置即代码”的确定性。真正的健康检查应该放在应用启动后的/health端点里。2.3 Model I/O层的核心契约不只是“发消息收回复”Model I/O不是函数调用而是一份双向协议。我们定义其核心契约如下要素输入侧要求输出侧要求违反后果消息结构必须是List[{role: user/system/assistant, content: str}]system仅允许出现在首条且长度≤2048字符响应必须含choices: [{message: {role: assistant, content: str}}]content不能为空字符串解析器抛出InvalidMessageFormatError触发fallback流程参数传递temperature,top_p,max_tokens必须显式传入禁止使用**kwargs透传响应头必须返回x-ratelimit-remaining和x-model-latency否则视为降级响应日志标记[WARN] missing_latency_header告警推送至企业微信错误处理客户端需捕获openai.RateLimitError、openai.APIConnectionError等具体异常模型返回{error: {code: context_length_exceeded}}时必须自动截断输入并重试重试次数上限为2次超限则返回用户友好提示“内容过长请精简后重试”这个契约的威力在于它让“模型”从一个黑盒变成了一个可预期的组件。当你后续要接入某实验室自研的小模型时只需确保它的HTTP API返回符合此契约的JSON上层Agent逻辑一行代码都不用改。我在某高校项目中用这套契约两周内完成了从GPT-4到本地部署的Qwen1.5-7B的平滑切换零业务逻辑修改。3. 核心细节与实操要点从代码到可运行的最小闭环3.1 初始化脚本详解init_project.py的每一行都在解决什么问题我们不写main.py而是先写一个纯粹的初始化脚本。以下是经过生产环境验证的init_project.py核心片段逐行解释其设计意图# init_project.py import os import sys from pathlib import Path import logging from typing import Optional # 第1步强制路径规范 —— 解决“为什么我的config读不到”的元问题 ROOT_DIR Path(__file__).resolve().parent.parent # 固定为项目根目录 sys.path.insert(0, str(ROOT_DIR)) # 确保后续import从根开始 os.chdir(ROOT_DIR) # 强制工作目录避免相对路径灾难 # 第2步环境变量预检 —— 不是简单读取而是带语义校验 def load_env(): required [OPENAI_API_KEY, MODEL_PROVIDER] # 明确声明依赖 missing [k for k in required if not os.getenv(k)] if missing: raise EnvironmentError(fMissing required env vars: {missing}) if os.getenv(MODEL_PROVIDER) not in [openai, qwen, local]: raise ValueError(fUnsupported MODEL_PROVIDER: {os.getenv(MODEL_PROVIDER)}) # 第3步日志初始化 —— 结构化是底线不是可选项 def setup_logging(): import structlog structlog.configure( processors[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmtiso), structlog.processors.StackInfoRenderer(), # 关键出错时自动带栈 structlog.processors.format_exc_info, structlog.processors.JSONRenderer() # 强制JSON杜绝日志解析失败 ], context_classdict, logger_factorystructlog.stdlib.LoggerFactory(), ) return structlog.get_logger() # 第4步配置加载 —— 分层合并且带类型安全 from pydantic import BaseModel, Field from typing import Literal class LLMConfig(BaseModel): model_name: str Field(defaultgpt-4-turbo) temperature: float Field(default0.3, ge0.0, le2.0) max_tokens: int Field(default2048, gt1, le4096) class AppConfig(BaseModel): env: Literal[dev, prod] dev llm: LLMConfig LLMConfig() def load_config() - AppConfig: config_path Path(config) / f{os.getenv(ENV, dev)}.py if not config_path.exists(): raise FileNotFoundError(fConfig not found: {config_path}) # 动态导入配置模块而非exec避免代码注入风险 import importlib.util spec importlib.util.spec_from_file_location(config, config_path) config_module importlib.util.module_from_spec(spec) spec.loader.exec_module(config_module) return AppConfig(**config_module.CONFIG_DICT) if __name__ __main__: load_env() logger setup_logging() config load_config() logger.info(Project initialized, configconfig.model_dump())这段代码解决的不是“怎么跑起来”而是“怎么确保每次跑起来的行为都一致”。比如os.chdir(ROOT_DIR)这一行看似简单却避免了90%的“找不到config文件”、“路径拼接错误”类问题。再比如importlib.util.spec_from_file_location替代exec是因为某公司曾因配置文件被注入恶意代码导致所有Agent实例静默上传用户数据——安全不是选配是初始化的第一道门。3.2 Model I/O实现llm_client.py中的五个关键决策真正的功夫在llm_client.py。以下是核心类OpenAIClient的关键实现每个方法都对应一个实战决策# llm_client.py import openai import time import json from typing import List, Dict, Any, Optional from tenacity import retry, stop_after_attempt, wait_exponential class OpenAIClient: def __init__(self, api_key: str, base_url: Optional[str] None): self.client openai.AsyncOpenAI( api_keyapi_key, base_urlbase_url, # 支持指向本地Ollama或vLLM服务 timeoutopenai.Timeout(30.0), # 显式超时不依赖默认值 ) self._request_counter 0 # 用于生成request_id retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max10), reraiseTrue ) async def generate( self, messages: List[Dict[str, str]], model_name: str, temperature: float 0.3, max_tokens: int 2048, **kwargs ) - Dict[str, Any]: # 决策1严格校验输入消息结构 self._validate_messages(messages) # 决策2构造标准请求体强制添加request_id request_id freq_{int(time.time())}_{self._request_counter} self._request_counter 1 # 决策3参数白名单控制防止意外透传 valid_params { model: model_name, messages: messages, temperature: temperature, max_tokens: max_tokens, } # 只允许传入明确声明的参数忽略其他kwargs valid_params.update({k: v for k, v in kwargs.items() if k in [top_p, frequency_penalty, presence_penalty]}) start_time time.time() try: response await self.client.chat.completions.create(**valid_params) # 决策4标准化响应结构抹平OpenAI API版本差异 # v1.0 返回Pydantic对象需转dict result { request_id: request_id, model: response.model, choices: [{ message: { role: response.choices[0].message.role, content: response.choices[0].message.content or } }], usage: { prompt_tokens: response.usage.prompt_tokens, completion_tokens: response.usage.completion_tokens, total_tokens: response.usage.total_tokens }, latency_ms: int((time.time() - start_time) * 1000) } # 决策5主动注入可观测字段不依赖响应头 result[x_model_latency] result[latency_ms] result[x_ratelimit_remaining] getattr( response, _headers, {} ).get(x-ratelimit-remaining, unknown) return result except openai.RateLimitError as e: # 统一错误处理便于上层分类 raise ModelRateLimitError(fRate limit exceeded: {str(e)}) from e except Exception as e: raise ModelCallError(fModel call failed: {str(e)}) from e def _validate_messages(self, messages: List[Dict[str, str]]) - None: if not messages: raise ValueError(messages list cannot be empty) if messages[0].get(role) ! system: # 允许无system消息但若存在必须为首条 pass for msg in messages: if role not in msg or content not in msg: raise ValueError(Each message must have role and content) if not isinstance(msg[content], str): raise TypeError(fcontent must be str, got {type(msg[content])})这五个决策每一个都来自血泪教训决策1的校验某开发者在调试时误将{text: hi}传入导致openai.BadRequestError但错误信息极其晦涩花3小时才定位。决策2的request_id没有它当用户说“刚才那条回复错了”你根本无法在日志里捞出对应请求。决策3的参数白名单openai库会把未知参数静默丢弃但某国产模型SDK却会因seed参数不存在而直接崩溃。决策4的标准化OpenAI API在v0.x和v1.x间返回结构不兼容手动转换避免上层逻辑反复适配。决策5的主动注入云服务商的响应头可能被CDN剥离主动计算latency_ms是唯一可靠方式。注意tenacity重试策略中reraiseTrue是关键。很多教程用return None吞掉异常结果上层看到空响应却不知原因。我们必须让错误浮出水面由Agent逻辑决定是重试、降级还是报错给用户。3.3 最小可运行Demo三步验证你的I/O层是否真正就绪初始化和I/O写完必须立刻验证。以下是一个不依赖任何Agent框架的纯验证脚本它只做一件事证明你的管道是通的。# demo/verify_io.py import asyncio import json from init_project import load_env, setup_logging from llm_client import OpenAIClient async def main(): load_env() logger setup_logging() # Step 1: 实例化客户端使用真实API Key client OpenAIClient( api_keyos.getenv(OPENAI_API_KEY), base_urlNone # 直连OpenAI ) # Step 2: 构造最简合规消息 messages [ {role: system, content: 你是一个严谨的校验助手只回答校验通过不加任何其他字。}, {role: user, content: 请校验} ] try: # Step 3: 调用并打印完整响应 result await client.generate( messagesmessages, model_namegpt-4-turbo, temperature0.0, # 用0.0确保确定性 max_tokens50 ) logger.info(I/O Verification Success, request_idresult[request_id], contentresult[choices][0][message][content], latencyresult[latency_ms], tokensresult[usage][total_tokens]) # 验证核心契约 assert 校验通过 in result[choices][0][message][content], Content mismatch assert result[latency_ms] 0, Latency must be positive assert result[usage][total_tokens] 0, Token count must be positive print(✅ I/O Layer Verified: All contracts satisfied.) except Exception as e: logger.error(I/O Verification Failed, errorstr(e)) print(f❌ Verification failed: {e}) if __name__ __main__: asyncio.run(main())运行它你应该看到控制台输出✅ I/O Layer Verified...日志文件中有一条结构化JSON包含request_id、content、latency_ms、total_tokens如果你把model_name改成不存在的gpt-999会看到清晰的ModelNotFoundError而不是一堆traceback这个Demo的价值在于它把“能跑”和“跑得对”分开。很多项目卡在“能跑”却从未验证“跑得对”。而真正的工程化始于每一次调用都符合契约。4. 实操过程与核心环节实现从零到可调试的个人助手雏形4.1 创建项目骨架比mkdir多做的四件事别急着写代码先用命令行创建一个经得起时间考验的骨架。以下是我在某实验室带学生时的标准流程比mkdir多做的四件事每一件都直击协作痛点# 1. 创建带Git签名的仓库避免后续commit作者混乱 git init --initial-branchmain git config user.name PersonalAI-Team git config user.email no-replypersonalai.local # 2. 创建标准化目录结构注意config/下不放.py放.pyi存根 mkdir -p src/{core,agents,utils,models} tests/ config/ docs/ data/ # 3. 初始化配置存根防新人乱改 echo # config/base.pyi from typing import Literal from pydantic import BaseModel class LLMConfig(BaseModel): model_name: str temperature: float max_tokens: int class AppConfig(BaseModel): env: Literal[dev, prod] llm: LLMConfig config/base.pyi # 4. 生成.gitignore重点屏蔽IDE缓存和模型权重 echo venv/ __pycache__/ *.pyc .env data/models/ # 本地模型权重不进Git *.safetensors # 大模型权重文件 .gitignore这四件事解决了什么git config避免某同学用自己GitHub账号commit导致CI/CD权限混乱config/base.pyi.pyi是Python存根文件它不执行只提供类型提示新人打开config/dev.py时IDE会自动提示字段名和类型杜绝config.llm.temperture这种低级拼写错误.gitignore里的data/models/某公司项目曾因误提交10GB模型权重导致Git仓库膨胀到无法克隆从此成为铁律。4.2 编写第一个可交互的助手cli_assistant.py现在把初始化和I/O串起来做一个命令行版个人助手。它不炫技但必须可调试、可追踪、可复现# cli_assistant.py import asyncio import sys from init_project import load_env, setup_logging, load_config from llm_client import OpenAIClient async def run_cli(): load_env() logger setup_logging() config load_config() # 实例化客户端复用初始化逻辑 client OpenAIClient(api_keyos.getenv(OPENAI_API_KEY)) print( 个人AI助手已启动输入 quit 退出) print( 提示输入 debug 查看最近一次请求详情\n) while True: try: user_input input(You: ).strip() if user_input.lower() in [quit, exit, q]: print( 再见) break if not user_input: continue # 构造消息此处简化实际项目应有更复杂的system prompt管理 messages [ { role: system, content: 你是一个专注、简洁、有用的个人助手。回答时直接给出核心信息不加寒暄。 }, {role: user, content: user_input} ] # 记录开始时间用于对比 start_time asyncio.get_event_loop().time() result await client.generate( messagesmessages, model_nameconfig.llm.model_name, temperatureconfig.llm.temperature, max_tokensconfig.llm.max_tokens ) # 计算并显示耗时用户感知的延迟 elapsed int((asyncio.get_event_loop().time() - start_time) * 1000) print(fAI: {result[choices][0][message][content]}) print(f[⏱️ {elapsed}ms | {result[usage][total_tokens]} tokens]\n) # 存储最近一次结果供debug命令调用 if last_result not in locals(): last_result None last_result result except KeyboardInterrupt: print(\n 强制退出) break except Exception as e: print(f❌ 处理失败: {e}) logger.exception(CLI execution error, errore) if __name__ __main__: asyncio.run(run_cli())运行python cli_assistant.py你会得到一个极简但极度透明的交互界面。它的价值不在功能而在可观测性每次响应后明确告诉你耗时多少毫秒、用了多少token输入debug时你可以打印出last_result看到完整的request_id、原始响应体、header信息所有异常都记录到结构化日志方便后续分析。我坚持认为一个AI助手的成熟度不取决于它能聊多深而取决于你能否在10秒内定位到“为什么它这次回复错了”。这个CLI就是你的第一双眼睛。4.3 集成日志与监控让每一次调用都“看得见”初始化时定义的日志契约必须在真实调用中兑现。以下是cli_assistant.py中日志记录的关键增强替换原print部分# 替换原print部分加入结构化日志 logger.info( Assistant interaction, request_idresult[request_id], user_inputuser_input, ai_responseresult[choices][0][message][content], modelresult[model], latency_msresult[latency_ms], prompt_tokensresult[usage][prompt_tokens], completion_tokensresult[usage][completion_tokens], total_tokensresult[usage][total_tokens], # 添加业务上下文标签 contextcli_interactive ) # 同时记录性能指标为后续Prometheus埋点 if hasattr(logger, metrics): logger.metrics.histogram( assistant.latency_ms, result[latency_ms], tags{model: result[model]} ) logger.metrics.counter( assistant.token_total, result[usage][total_tokens], tags{direction: out} )这里的关键是日志不是为了“看”而是为了“查”和“算”。当你在某公司项目中接到“助手响应变慢”的告警时你不需要登录服务器翻日志而是直接在Grafana里画一条histogram_quantile(0.95, rate(assistant_latency_ms_bucket[1h]))曲线一眼看出P95延迟是否突破阈值。而这一切都始于初始化时对日志字段的强制约定。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题速查表高频故障与一招定位法问题现象可能原因一招定位法解决方案调用永远返回空字符串content字段为None而代码未处理在generate()返回前加print(fRaw content: {response.choices[0].message.content})修改响应解析逻辑增加or 兜底日志里request_id全是req_0_0_request_counter未正确实例化或多个客户端共享同一实例在__init__中print(fCounter init: {self._request_counter})确保每个OpenAIClient实例独占counter或改用线程安全的threading.local()temperature0.0时仍输出随机内容某国产模型不支持temperature0.0自动忽略该参数用curl手动调用模型API传入{temperature: 0.0}观察响应在I/O层增加参数兼容性映射表如qwen模型将temperature0.0转为top_k1max_tokens100但实际返回200 token模型忽略max_tokens或usage字段统计的是输入token检查result[usage][completion_tokens]是否合理在I/O层增加后置截断content[:max_tokens*3]按UTF-8字节粗略估算system消息被模型完全无视某些模型如早期Llama2不支持system角色将system内容拼接到首条user消息前格式为System: {content}\n\nUser: {user_input}在_validate_messages中根据model_name动态转换消息结构这张表里的每一个问题都来自真实项目。比如“system被无视”这个问题某高校团队在用Llama2-13B微调时连续三天以为是prompt写错最后发现是模型根本不吃system角色——这种坑只有亲手调过至少3种模型的人才会知道。5.2 实操避坑心得来自深夜调试现场的3条铁律铁律1永远不要信任模型返回的usage字段OpenAI官方文档明确写着“usage字段是估算值可能与实际不符”。我在某跨平台系统中做过压测当max_tokens100时completion_tokens返回87但实际响应体UTF-8长度是312字节按平均3字节/词元算真实消耗约104词元。这意味着如果你用usage做token计费误差可能达15%。解决方案用tiktoken库在本地精确计算encoding.encode(response_text)虽然慢10ms但换来的是财务级准确度。铁律2temperature不是“随机度”而是“分布平滑度”很多教程说“temperature0最确定1.0最随机”这是严重误导。真实情况是temperature作用于logits公式为softmax(logits / temperature)。当temperature0.1时最高logit会被极度放大低logit几乎归零当temperature2.0时所有logit被拉平模型像在掷骰子。我在某法律文书生成项目中发现temperature0.7生成的条款最符合律师审阅习惯而0.3反而因过度保守出现逻辑断层。结论temperature没有“好”“坏”只有“适配场景”必须针对你的输出类型做AB测试。铁律3重试不是万能的要区分“可重试”和“不可重试”错误tenacity重试很诱人但必须精准分类可重试RateLimitError、APIConnectionError、Timeout——网络抖动重试大概率成功不可重试BadRequestError参数错、AuthenticationErrorKey错、PermissionDeniedError权限不足——重试100次也是错。我在某公司项目中吃过亏把AuthenticationError也放进重试结果API Key错误时服务每秒发起3次无效请求触发了OpenAI的暴力探测防护整个IP被临时封禁。教训重试策略必须和错误类型强绑定宁可少重试不可乱重试。5.3 性能调优实录如何把单次调用从1200ms压到380ms在某图像处理Demo中我们对I/O层做了三次关键优化效果如下优化项优化前优化后原理说明HTTP连接池复用每次新建aiohttp.ClientSession复用全局sessionTCP握手耗时从~150ms降至5ms尤其在高并发时显著响应流式解析等待完整响应体再json.loads()使用aiohttp的content.iter_any()边收边解析减少内存拷贝首字节延迟TTFB从800ms降至210msPrompt压缩原样发送system消息含冗余空格/换行用re.sub(r\s, , system_content).strip()预处理减少约12%的prompt token间接降低传输和处理耗时最终P50延迟从1200ms降至380msP95从2100ms降至620ms。这不是靠换更快的模型而是靠抠细节。真正的工程能力就藏在这些不显眼的毫秒里。6. 后续演进路径从阶段1到真正可用的个人助手阶段1的终点不是“能用了”而是“可以放心地加功能了”。基于这个坚实基础后续演进有三条清晰路径路径A加记忆Memory当前I/O层已提供request_id和结构化日志下一步可基于request_id构建会话存储。例如用Redis存{request_id: {messages: [...], timestamp: ...}}再在generate()前自动注入最近3轮对话。关键是记忆加载必须作为I/O层的前置钩子而非Agent逻辑的一部分确保所有模型调用都享有同等记忆能力。路径B加工具ToolsI/O层的messages结构已预留扩展空间。当需要调用搜索工具时不是让模型直接输出URL而是定义tool_calls字段由I/O层解析后调用对应函数再将结果以tool_response角色塞回消息流。这样工具调用就变成了I/O协议的一部分上层Agent只需关注“该不该调”不关心“怎么调”。路径C加多模型路由Router当前model_name是硬编码参数下一步可升级为model_router.py根据输入内容自动选择模型短文本问答走轻量模型快长文档摘要走