
在 AI Agent 的开发中Agent Skills 是最近被反复提到的关键概念它解决的是模型与真实能力之间如何规范衔接的问题。很多团队做第一个 Agent 时习惯把所有能力直接写进 system prompt或者用一串 if-else 去判断用户意图这在技能只有两三个时还能跑通。一旦技能数量变多、用户问题变复杂模型不知道什么时候该调用什么能力调用完也不知道结果怎么回填整个流程很快就会变得不可维护。Agent Skills 的核心思路是把“模型能做什么”和“模型应该做什么”拆开每个能力封装成一个带名称、描述、参数说明和执行函数的技能包由模型在对话中动态决定调用顺序和参数。这篇文章会用一套可运行的 Python 最小实现把技能封装、Agent 调度和项目落地三个阶段完整走一遍。适合阅读的是已经调过大模型 API、知道 system prompt 和 user prompt 是什么但还没有系统性做过 Agent 项目的开发者。文中使用 OpenAI 兼容接口作为示例不绑定特定云厂商如果你用的是本地部署模型或其他兼容服务只需要修改base_url、api_key和model三个配置。学完之后你可以把这套结构复用到知识库问答、客服助手、文档处理、数据统计等场景中。1. Agent Skills 到底是什么从函数调用到技能封装1.1 一句话理解它解决什么问题用一句话概括Agent Skills 是把模型的“理解能力”和程序的“执行能力”连接起来的标准化封装。普通情况下大模型只能生成文本不能真正查询数据库、读取文件、调用接口。Agent Skills 做的事情是把“查询数据库”“读取文件”“调用接口”这些能力包装成模型能看懂、能选择、能传参的独立技能包。用户说一句话后模型判断需要哪个技能把意图翻译成结构化参数交给程序执行再把执行结果作为上下文生成最终回答。所以它不是某个具体 API而是一套组织代码的方式。底层依赖的是大模型平台提供的函数调用机制也就是常说的 Function Calling 或 Tool Calling上层则需要在工程上设计技能的注册、描述、执行、异常处理和日志记录。两者结合才是完整可落地的 Agent Skills。1.2 底层机制和工程抽象的分工理解 Agent Skills 之前要先分清楚两层概念。底层机制是函数调用。以 OpenAI 兼容接口为例业务代码可以在请求中额外传入一个tools数组数组里每个元素描述一个函数函数名、函数说明、参数类型和必填项。模型收到用户消息后不会直接去执行任何代码而是返回一个结构化的调用指令包含调用的函数名和参数字典。真正执行代码的是业务系统。上层工程抽象才是 Agent Skills。函数调用只是协议层的“通道”而技能是业务层的“封装单元”。一个技能除了包含函数本身还应该包含技能名称和别名。面向模型的功能描述说明这个技能在什么场景下使用。参数 JSON Schema明确每个参数的类型、含义和示例。执行函数以及函数内部的超时、异常处理逻辑。技能是否启用的开关。把这两层分开后Agent 的调度逻辑就变得非常简洁模型负责决策Registry 负责找到并执行技能Agent 循环负责把调用结果回填到上下文最终由模型生成答案。1.3 为什么说硬编码调度撑不住真实项目有的团队会写类似下面的代码来模拟 Agent 行为if 天气 in user_input: result weather_query(city北京) elif 新闻 in user_input: result news_query() else: result 暂时无法处理这种写法在技能在两个以内时看起来挺直接但一旦技能变多问题就出现了。用户说“帮我查一下北京明天适合穿什么”模型要拆出“查询天气”和“查询穿衣建议”两个技能硬编码无法覆盖这种组合意图。用户问题的表述千变万化“查天气”“天气怎么样”“明天冷不冷”都对应同一个技能靠关键词匹配很容易漏。新增一个技能时需要改主流程技能之间无法复用也无法做灰度开关和版本管理。Agent Skills 的价值就在这个阶段体现把“判断”交给模型把“执行”交给代码。模型根据用户输入和技能描述自行匹配代码只负责注册技能和稳定执行。这样技能数量从三个增加到三十个Agent 主循环代码基本不用变。2. 搭建最小运行环境依赖、目录和三个核心数据结构2.1 环境要求和 Python 依赖本文的示例以 Python 3.10 及以上版本为基准因为新版本的类型注解和dataclass使用体验更好。运行环境只需要一台能访问大模型接口的机器不依赖 GPU如果使用本地模型你需要保证本地服务暴露了兼容/v1/chat/completions的接口。建议创建一个独立虚拟环境避免污染系统 Pythonpython -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate安装依赖pip install openai1.30.0 python-dotenv1.0.0openai客户端库不仅支持 OpenAI 官方接口也支持绝大多数兼容接口的平台因此一个客户端就能适配多种模型来源。python-dotenv用于读取.env文件中的密钥配置避免把密钥写死在代码里。注意不同平台对函数调用协议的支持程度不完全一样。落地前要先确认你的模型服务是否支持tools参数以及支持到哪个版本。部分早期模型只支持文本补全不支持结构化函数调用。2.2 项目目录结构为了让技能与调度逻辑解耦示例项目按下面的目录组织agent_skills_demo/ ├── requirements.txt ├── .env ├── skills/ │ ├── __init__.py │ ├── base.py # Skill 数据结构 │ ├── registry.py # 技能注册表 │ ├── docs_skill.py # 文档检索技能 │ └── calc_skill.py # 安全计算技能 ├── agent/ │ ├── __init__.py │ ├── llm_client.py # 统一 LLM 客户端 │ └── core.py # Agent 调度主循环 ├── data/ │ └── docs/ # 本地知识文档 └── main.py # 程序入口这个目录划分的原则是skills目录只放技能本身agent目录只放调度和模型调用逻辑data目录放技能操作的业务数据。这样后续新增技能时不需要改动 Agent 主循环更换模型服务商时也不需要改动技能代码。2.3 Skill、SkillRegistry、Agent 三个结构的职责划分整个最小系统由三个核心类组成类名职责关键能力Skill描述一个技能包的元数据和执行函数把技能转成符合平台协议的tools格式SkillRegistry统一管理技能注册、查询和禁用状态register注册、list_schemas输出模型可读列表、execute执行技能Agent与模型交互完成调度循环解析模型返回的tool_calls执行技能回填结果三者之间是单向依赖Agent依赖SkillRegistrySkillRegistry依赖Skill。任何一层都不应该反向依赖。尤其要注意Agent不应该直接 import 具体的技能实现否则每新增一个技能都要改 Agent就回到了硬编码的老路。3. 从零实现 Agent 调度主流程3.1 先写技能注册表统一管理能力清单第一步实现Skill数据结构和SkillRegistry。Skill使用dataclass保存技能元数据并提供to_tool_schema方法把内部格式翻译成模型平台要求的工具格式# skills/base.py from dataclasses import dataclass from typing import Callable, Dict dataclass class Skill: name: str description: str parameters: Dict handler: Callable enabled: bool True def to_tool_schema(self) - Dict: return { type: function, function: { name: self.name, description: self.description, parameters: self.parameters, }, }这里把parameters保存为原生 JSON Schema 字典而不是单独字段是因为大多数模型平台直接接受 JSON Schema 格式保留原样可以减少转换成本。注册表负责按名称查找技能、输出所有可用技能的协议格式并统一执行入口# skills/registry.py from typing import Any, Dict, List from skills.base import Skill class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill) - None: if skill.name in self._skills: raise ValueError(f技能 {skill.name} 已存在请检查注册逻辑) self._skills[skill.name] skill def get(self, name: str) - Skill: return self._skills.get(name) def list_schemas(self) - List[Dict]: return [ skill.to_tool_schema() for skill in self._skills.values() if skill.enabled ] def execute(self, name: str, arguments: Dict) - Any: skill self.get(name) if skill is None or not skill.enabled: raise KeyError(f技能不存在或未启用: {name}) return skill.handler(**arguments)execute方法里先查技能是否存在再调用处理函数。这样所有技能都走同一个执行入口后续加日志、加耗时统计、加权限校验都只需要改这一处。3.2 再写 LLM 客户端统一入口方便换模型LLM 客户端封装成独立类业务代码不直接操作openai库避免在 Agent 主循环里散落模型调用的细节# agent/llm_client.py from openai import OpenAI from typing import Dict, List, Optional class LLMClient: def __init__(self, base_url: str, api_key: str, model: str): self.client OpenAI(base_urlbase_url, api_keyapi_key) self.model model def chat(self, messages: List[Dict], tools: Optional[List[Dict]] None): kwargs { model: self.model, messages: messages, } if tools: kwargs[tools] tools return self.client.chat.completions.create(**kwargs)这里保留tools的可选性是为了调试方便。排查问题时可以先用toolsNone验证模型是否能正常对话再逐步加入工具调用缩小问题范围。实际生产环境建议把timeout、max_tokens、temperature也通过参数暴露而不是写死在客户端里。3.3 核心循环让模型决定调哪个技能再把结果回填Agent 主循环是整个系统最关键的部分。它的核心逻辑是带tools列表调用模型如果模型返回tool_calls就逐个执行技能把结果以roletool的消息回填到对话历史再继续调用模型直到模型不再返回tool_calls才把最终文本返回给用户。# agent/core.py import json class Agent: def __init__(self, registry, llm, max_rounds: int 5): self.registry registry self.llm llm self.max_rounds max_rounds def run(self, user_input: str) - str: messages [ {role: system, content: 你是助手。需要工具时先调用工具获取信息再基于工具结果回答用户。}, {role: user, content: user_input}, ] for _ in range(self.max_rounds): response self.llm.chat(messages, toolsself.registry.list_schemas()) message response.choices[0].message if not message.tool_calls: return message.content or # 把模型这一轮的工具调用指令追加到历史消息 messages.append({ role: assistant, content: message.content or , tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments, }, } for tc in message.tool_calls ], }) # 逐个执行技能并回填结果 for tc in message.tool_calls: tool_name tc.function.name try: arguments json.loads(tc.function.arguments or {}) result self.registry.execute(tool_name, arguments) result_text json.dumps(result, ensure_asciiFalse, defaultstr) except Exception as exc: result_text json.dumps({error: str(exc)}, ensure_asciiFalse) messages.append({ role: tool, tool_call_id: tc.id, content: result_text, }) return 已达到最大调度轮数请拆分问题后重试。这个循环有三个细节需要重点解释。第一assistant 消息里必须带上原始tool_calls并且用roleassistant组织不能直接丢掉。平台协议要求后续的 tool 消息必须关联到某一次tool_call_id如果历史里缺少 assistant 的tool_calls声明组装第二轮请求时会报错。第二技能执行要包在try/except里但不是为了吞异常而是把异常转成 JSON 文本回填给模型。模型看到{error: ...}后可以自己决定是换一种方式调用还是向用户说明失败原因。如果直接让异常冒泡整个 Agent 就会中断用户只看到一段堆栈。第三必须设置max_rounds。模型在遇到复杂问题时可能会连续多次调用工具没有上限就会出现死循环既浪费时间也消耗额度。示例里默认 5 轮生产环境可以根据任务的复杂度调整但建议不要超过 10。3.4 注册两个可运行技能为了让示例能直接运行实现两个简单技能一个是本地文档搜索一个是安全四则运算。文档搜索技能从data/docs目录读取 Markdown 文件按关键词返回命中的文件名、行号和片段# skills/docs_skill.py from pathlib import Path def search_docs(keyword: str) - list: docs_dir Path(data/docs) results [] if not docs_dir.exists(): return results for file in docs_dir.glob(*.md): lines file.read_text(encodingutf-8).splitlines() for index, line in enumerate(lines, start1): if keyword in line: results.append({ file: file.name, line: index, snippet: line.strip()[:120], }) return results[:10]计算技能使用ast模块做安全求值只允许常量、四则运算和幂运算不使用eval避免执行恶意表达式# skills/calc_skill.py import ast import operator _OPERATORS { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, } def _safe_eval(node): if isinstance(node, ast.Expression): return _safe_eval(node.body) if isinstance(node, ast.Constant): if isinstance(node.value, (int, float)): return node.value raise ValueError(不支持的常量类型) if isinstance(node, ast.BinOp): op _OPERATORS.get(type(node.op)) if op is None: raise ValueError(不支持的操作符) return op(_safe_eval(node.left), _safe_eval(node.right)) raise ValueError(只支持简单四则运算表达式) def calculate(expression: str) - float: tree ast.parse(expression, modeeval) return _safe_eval(tree)这两个技能一个是面向业务的真实能力一个是基础运算能力足够演示模型如何在多个技能之间做选择。3.5 启动入口和首次运行在main.py里把技能注册进 Registry再创建 Agent进入命令行交互# main.py from skills.base import Skill from skills.registry import SkillRegistry from skills.docs_skill import search_docs from skills.calc_skill import calculate from agent.llm_client import LLMClient from agent.core import Agent def build_registry() - SkillRegistry: registry SkillRegistry() registry.register(Skill( namesearch_docs, description在本地知识文档中按关键词搜索返回文档名、行号和文本片段。当用户询问政策、制度、术语或历史记录时使用。, parameters{ type: object, properties: { keyword: { type: string, description: 要搜索的关键词例如报销、年假、考勤, } }, required: [keyword], }, handlersearch_docs, )) registry.register(Skill( namecalculate, description执行简单的四则运算表达式返回计算结果。当用户需要算数或统计数据时使用。, parameters{ type: object, properties: { expression: { type: string, description: 合法数学表达式例如500 * 4 100, } }, required: [expression], }, handlercalculate, )) return registry if __name__ __main__: registry build_registry() llm LLMClient( base_urlhttps://your-endpoint.example.com/v1, api_keyyour-api-key, modelyour-model-name, ) agent Agent(registry, llm) while True: user_input input(请输入问题输入 exit 退出) if user_input.strip().lower() exit: break answer agent.run(user_input) print(\nAgent 回答) print(answer)base_url、api_key和model三个值在示例里是占位符实际运行时建议从.env读取LLM_BASE_URLhttps://your-endpoint.example.com/v1 LLM_API_KEYyour-api-key LLM_MODELyour-model-name在main.py中通过os.getenv读取并使用python-dotenv加载.env文件。这样做的好处是切换模型服务商时不需要改代码。4. 技能封装容易踩的细节描述、参数、异常和状态4.1 技能描述怎么写模型才容易选对模型判断“该不该调某个技能”主要依据是技能的名字和描述。描述写得太抽象模型不知道什么时候用写得太啰嗦会挤占上下文空间还可能让模型产生误判。一个实用的描述结构是“做什么 什么时候用 输入是什么”。比如错误写法搜索文档推荐写法在本地知识文档中按关键词搜索返回文档名、行号和文本片段。当用户询问政策、制度、术语或历史记录时使用。推荐写法里包含了功能、返回内容和触发场景。模型读到“政策、制度、术语”这几个词时就能更容易把用户问题映射到这个技能上。4.2 参数 Schema 的完整度决定传参质量定义技能参数时每个字段都要提供type和description必填项要放进required数组。参数的描述要尽量给出取值范围或示例例如keyword的说明是“例如报销、年假、考勤”模型生成参数时会参考这个示例。参数写法效果{type: string}模型只知道传字符串不知道传什么{type: string, description: 城市名称例如北京、上海}模型能生成符合预期的值{type: object, properties: {...}, required: [city]}缺少city时平台协议会报错适合强制校验参数少而精比参数多而全更好。模型在函数调用时并不是每次都严格按照说明生成参数参数越多越容易生成错误结构。能合并的参数尽量合并不能让模型决策的字段就固定成全函数内部的默认值。4.3 技能内部异常必须转成模型可读的结果技能执行时可能遇到文件不存在、网络超时、参数越界等异常。这些异常必须被捕获并转成结构化的错误结果返回给模型。Agent 主循环里已经有统一的try/except但更推荐在技能内部先做一层兜底把业务错误转成语义明确的内容。例如搜索技能可以这样处理def search_docs(keyword: str) - dict: if not keyword or not keyword.strip(): return {error: keyword 不能为空} # 正常搜索逻辑 return {results: results}把错误作为返回值而不是抛异常模型能直接读到“keyword 不能为空”下一轮就会重新生成参数如果直接抛异常用户只看到一段堆栈对话体验会变得很差。4.4 无状态优先有状态要显式管理技能设计要尽量保持无状态同一个技能同样的参数应该得到同样的结果。无状态技能最容易复用也最容易做并发和缓存。如果某个技能必须依赖状态比如“保存当前用户的选择项”“记录上一步操作结果”不要把这个状态藏在技能内部。建议由 Agent 层维护一个上下文对象把需要跨轮保存的数据显式传递下去技能本身仍然保持无状态。这样即使 Agent 崩溃重启也能通过上下文对象恢复会话。5. Agent 调度策略从单技能调用到多技能协同5.1 三种常见调度模型原生调用、工作流、规划器Agent Skills 本身只是能力封装怎么调度这些技能才是决定项目体验的核心。常见调度方式有三种。原生函数调用是第一种。模型在每一轮根据用户输入和tools列表自行选择技能适合开放式问答但模型可能随机选择不合适的技能行为不够稳定。工作流是第二种。预先定义好技能的调用顺序比如“先检索文档再总结再翻译”每个步骤是确定的。这种方式稳定可控适合业务流程固定的场景例如工单自动分类、审批流转。规划器是第三种。模型先根据用户目标生成一个多步骤计划然后逐步执行计划中的技能每执行一步判断是否调整下一步。这种方式适合“用户只给最终目标过程需要拆解”的场景。比如做行业研究报告模型需要规划“先搜索行业背景再找头部公司再计算市场份额”。5.2 多技能协同时的优先级与兜底多个技能都能回答同一类问题时要给技能描述和业务规则设定优先级。比如用户问“报销标准是多少”可能同时命中“搜索文档”和“计算公式”。正确的做法是先搜索到标准再根据标准做计算而不是让模型在不知道标准的情况下直接算。兜底策略同样重要。当模型判断没有技能能处理当前问题时不能强行调用某个技能。System prompt 里要明确约束“如果没有合适的技能直接告诉用户当前能力范围不要编造。”否则模型会因为上下文里有技能列表为了完成任务而硬调一个不相关的工具。5.3 调度循环必须设置最大轮数和超时调度循环的稳定性直接决定生产可用性。至少要设置两个上限最大轮数和单次调用超时。最大轮数防止模型无限循环调用技能。前面示例里的max_rounds5就是干这件事。单次调用超时防止某个技能响应过慢拖垮整体体验。在 LLM 客户端里可以传timeout在执行外部 API 的技能里也应该设置自己的超时时间。技能执行时间超过阈值时要记录慢查询日志并考虑把技能改成异步任务。调度参数建议值调大的影响调小的影响max_rounds5 到 10能处理更复杂任务但耗时和费用上升复杂任务可能提前中断模型调用timeout30 到 60 秒更稳定但用户等待长容易误报超时技能执行timeout10 到 30 秒技能更稳定慢技能频繁失败6. 项目落地案例本地知识问答 Agent6.1 需求拆解和技能清单设计用前面实现的基础代码落地一个本地知识问答 Agent。需求是用户可以用自然语言查询本地知识文档中的制度信息并结合数值做简单统计。按需求拆解至少需要两个技能技能输入输出使用场景search_docs关键词文档名 行号 片段查询制度、政策、流程类信息calculate数学表达式数值结果根据查询到的数值做统计计算先准备两份简单的本地文档。data/docs/refund.md# 报销制度 报销申请需在费用发生后 30 天内提交。 单次报销上限为 500 元超出部分需部门经理审批。 报销单需附原始发票扫描件。data/docs/holiday.md# 年假政策 员工入职满一年后每年享有 10 天带薪年假。 年假可结转至次年但最多结转 3 天。6.2 运行案例并观察调度日志为了让调度过程可见可以在Agent.run中临时加入日志代码打印每一轮的模型决策和技能结果print(f[ROUND] 调用模型) print(f[TOOL_CALL] name{tool_name} args{arguments}) print(f[TOOL_RESULT] {result_text})真实项目里不建议用print应该用结构化日志。这里只是为了演示排查链路。启动程序后输入第一个问题请输入问题公司报销申请需要在多长时间内提交预期调度过程如下[ROUND] 调用模型 [TOOL_CALL] namesearch_docs args{keyword: 报销} [TOOL_RESULT] {results: [{file: refund.md, line: 3, snippet: 报销申请需在费用发生后 30 天内提交。}]} [FINAL] 公司规定报销申请需在费用发生后 30 天内提交。模型先调用search_docs查询关键词“报销”拿到文档片段后基于片段内容生成最终回答。注意整个流程中模型并没有直接“读到”完整文档它读到的只是技能返回的片段。所以技能返回内容的准确性决定了回答的准确性。6.3 验证数值统计场景输入第二个问题请输入问题单次报销上限 500 元报销 4 次一共是多少预期调度过程是两次工具调用[ROUND] 调用模型 [TOOL_CALL] namesearch_docs args{keyword: 报销上限} [TOOL_RESULT] {results: [{file: refund.md, line: 5, snippet: 单次报销上限为 500 元}]} [TOOL_CALL] namecalculate args{expression: 500 * 4} [TOOL_RESULT] 2000 [FINAL] 按单次报销上限 500 元计算报销 4 次合计 2000 元。这个例子展示了技能组合的价值文档搜索负责拿到业务规则计算技能负责执行运算两个技能通过 Agent 循环协作完成了一个单靠搜索或单靠计算都无法完成的任务。这也是 Agent Skills 相对传统对话框的核心优势用户不需要分步输入模型会自动拆解。6.4 验证异常分支再输入一个文档里没有的信息请输入问题公司团建频率是多少技能只配置了search_docs和calculate文档里也没有相关内容。预期模型返回类似“当前知识库中没有关于团建频率的信息建议联系行政确认”的答案而不是强行编造一个频率。如果模型编造了答案就要检查 system prompt 里是否缺少“没有资料时如实说明”的约束。7. 常见问题排查从现象倒推原因7.1 模型始终不调用技能现象无论用户问什么模型都直接用通用知识回答完全不上报tool_calls。排查顺序如下确认请求里真的传了tools。在 LLM 客户端加一行日志打印kwargs.get(tools)确认列表不为空。确认技能的enabled是True。list_schemas会过滤掉未启用的技能。确认模型平台支持函数调用。部分模型的轻量版本或旧版本接口不支持tools参数或静默忽略。检查技能描述是否太模糊。模型判断不出触发条件就更倾向于不调用。处理建议先用一个最简单的技能测试例如calculate输入“3 加 5 等于几”。如果这个都不调问题基本出在平台协议或请求构造上如果这个能调而复杂技能不能调问题出在技能描述上。7.2 arguments 不是合法 JSON现象模型返回了tool_calls但执行json.loads(tc.function.arguments)时报错或技能拿到的参数是None。常见原因有两个。一是平台返回的arguments本身就是空字符串这在部分模型上是正常的代码里要用tc.function.arguments or {}兜底。二是模型生成的 JSON 里带了多余的文本比如把注释或前后缀也拼进去。处理建议不要只依赖json.loads加一层宽松解析把字符串里的代码块标记剥掉再解析。同时要在日志里记录原始arguments字符串方便回溯。问题现象可能原因检查方式处理建议arguments解析失败模型返回空字符串或带多余文本打印tc.function.arguments原始值使用兜底or {}必要时做清洗解析技能得到错误参数description里没写示例对比参数 Schema 和实际传参在参数说明中增加示例值工具结果丢失没有保存 tool 消息检查 messages 数组长度确保每条roletool都有关联tool_call_id7.3 技能执行慢拖垮整体响应现象单轮对话要十几秒甚至更久用户明显感觉卡顿。排查先看耗时分布。在 Agent 主循环里记录两段时间模型调用耗时、技能执行耗时。如果技能耗时占比高优先优化技能本身比如加缓存、用并发请求、把返回内容截短。如果模型调用耗时长要检查是否因为 history 里塞了太多历史工具结果导致模型生成长度变长。也可以考虑把不再需要的中间工具结果从历史里裁剪只保留最终摘要。7.4 工具结果太长导致上下文溢出现象多轮工具调用后请求报错提示超过模型上下文长度。技能返回内容要限制长度。示例里的snippet已经做了[:120]截断但实际项目中技能可能返回很长的 JSON。建议在Agent.run里为result_text设置最大长度超长的内容截断并追加提示“结果已截断如需完整内容请缩小查询范围”。更稳妥的办法是让技能自己做分页一次只返回概要用户需要详情时再调一次技能。8. 生产级项目落地工程化实践和扩展方向8.1 学习环境与生产环境必须做的差别学习 Demo 只要能跑通就行生产环境则需要补齐稳定性、安全性和可观测性。两者的差别可以用一张表概括维度学习 Demo生产环境密钥配置写死或.env环境变量、配置中心或密钥管理服务技能注册启动时写在代码里支持配置化注册和动态启停日志print结构化日志 请求追踪 ID异常处理try/except返回错误重试、降级、熔断、告警安全不校验任何输入权限校验、敏感操作确认、输入过滤工具输出直接回填截断、脱敏、审计生产环境尤其要注意外部效果类技能比如发邮件、下单、删除数据。这类技能不能只靠模型判断就执行必须在代码层面增加人工确认环节。模型可以生成待执行参数但真正的发送动作要等用户二次确认。8.2 技能版本管理、灰度与审计技能是代码也会迭代。每个技能要像接口一样做版本管理。改动技能实现时不要直接覆盖线上技能建议在 Registry 里加版本字段先在小流量灰度验证再全量开放。技能描述改动也属于版本变更因为描述的变化会直接影响模型的选择行为影响面可能比代码改动更大。审计方面每个技能调用都应该记录用户问题、模型生成的调用参数、技能返回结果、最终回答、耗时、模型版本和技能版本。一旦出现回答异常可以通过这些信息还原整个决策链路。8.3 发布前检查清单从 Demo 走向生产前建议逐项检查以下清单[ ] 技能列表支持配置化启停不修改代码就能关闭异常技能。