从聊天到工具:封装可复用AI技能的实战记录

发布时间:2026/10/22 3:37:58
从聊天到工具:封装可复用AI技能的实战记录 第二天我开始觉得聊天式AI有点“不够用了”。第一天还在体验对话的丝滑什么问题都能聊感觉什么都能搞定。可真当我想把AI塞进一条真实的工作流里比如自动整理会议纪要、批量生成标题、统一格式化一堆资料时发现除了把问题抛给它然后复制答案剩下的全靠人肉搬运。于是这一天的主题就变得非常明确从聊天到工具把AI技能封装成可复用的模块。所谓的“封装可复用AI技能”简单来说就是不再把AI当聊天机器人而是当一个可以被程序调用的函数。输入是结构化参数输出是固定格式的数据中间那些提示词、上下文、重试逻辑都封装在内部。这篇文章我完整记录一下第二天的实战过程从设计思路到代码实现再到实测踩坑希望能给同样在做AI应用工程化的朋友一点参考。1. 聊天和工具之间隔着的不是技术而是“约定”第二天一开始我先给自己提了一个问题聊天模式和工具模式真正的分界线到底在哪。如果只是把对话接口换成函数调用那本质上还是聊天只不过换了个壳。真正让AI从一个“对话者”变成一个“工具”的是输入输出的契约化。1.1 聊天模式的三宗罪无状态、无格式、无边界我第一天用下来的感受非常典型同一个任务问三遍得到三个不同的回答。比如我让AI帮我把一段长文本提炼成几个要点第一次给的是Markdown列表第二次变成了一段散文第三次干脆加上了表情。这还不是最头疼的更麻烦的是每次都得把背景重新说一遍。聊天模式最大的问题是上下文依赖人肉维护。每一次调用都像是和一个新同事对接你得把行业背景、术语含义、输出要求全部重新交代。而且对话流是线性的一旦中间某轮回答不满意后面所有内容都可能跑偏。这种模式用于灵感碰撞、头脑风暴完全没问题但一旦要嵌入自动化的业务流程就是一个灾难。1.2 工具的四个特征参数化、结构化、可重复、可组合我想清楚的第一个结论是工具的本质是约定。它对外公布一个接口声明自己接受什么参数、返回什么结构至于内部是怎么实现的调用方完全不需要关心。一个合格的AI技能至少要满足四个条件。参数化指的是每次调用只传入核心变化量公共背景、角色设定、格式要求全部内置结构化指的是输出必须是可解析的固定格式不能是一段自然语言可重复指的是相同输入应该得到稳定质量的结果不能忽好忽坏可组合指的是这个技能可以被其他流程调用甚至多个技能串联成一个更大的工具。这四个特征全部要靠封装来实现。1.3 第二天的目标拆解三个必须交付的东西想明白之后我把目标拆成了三件具体的交付物第一个是可复用的技能类带输入校验、调用逻辑和错误处理第二个是至少两个实际场景的技能实例不能只做框架不落地第三个是一套注册机制让多个技能可以被统一发现和调用。这个目标比我想象中要清晰得多。不是说要把AI用到多炫酷而是要先解决“能用”的问题。我告诉自己如果第二天结束能用代码完成一个技能从定义到注册再到调用的全流程并且输出稳定就算达标。2. 先定契约再写提示词输入输出设计是整个封装的核心动手写代码之前我强迫自己先做一件事把输入输出的契约写下来。很多人封装AI技能习惯先写提示词总觉得提示词是重点。但实测下来恰恰相反提示词是最容易改的东西输入输出结构才是最难改的东西。2.1 为什么顺序这么重要提示词可以调数据结构不好改我把这个道理用生活中的例子想了一遍提示词像是服务员的话术数据结构像是菜单上的菜品。话术可以随时调整但如果菜品本身定错了后面的后厨、备菜、上菜流程全得跟着变。具体到AI技能里输入结构定的是调用方要准备什么数据输出结构定的是下游怎么消费结果。如果一开始输出是一段纯文本后面发现程序需要JSON才能解析那所有的调用方都得跟着改。反过来如果你一开始就定义好输出JSON哪怕提示词写得差一点最多是字段值不对修正成本也低很多。2.2 输入侧设计区分“核心参数”和“固定上下文”我在设计第一个技能的时候刻意把输入分成两层。核心参数是每次调用都变化的比如一段待整理的原始文本、一个可选的语气偏好固定上下文是永远不变的比如技能的角色设定“你是一名会议纪要整理专家”、输出格式规范、常用的术语表。实际写类的时候固定上下文全部写在class的常量里核心参数通过方法的kwargs传入。这样有几个好处调用方不需要知道内部提示词长什么样技能升级内部提示词对外完全无感另外不同技能之间不会互相污染上下文。2.3 输出侧设计强制结构化但别过度依赖JSON输出设计我纠结了一下。现在很多模型接口都支持JSON格式输出但并不是所有场景都适合一上来就用JSON。原因有两个一是强制JSON有时会损失内容质量模型会把精力放在格式合规上而不是内容本身二是JSON解析对模型的格式纪律要求很高中文标点、缩进、转义都可能翻车。我采用的折中方案是JSON作为首要格式但在提示词里给出明确示例而不是只丢一句“请输出JSON”。同时所有输出在代码层做二次校验解析失败就重试重试还失败就降级返回原始文本并打上标记。这套逻辑后面的实战验证下来比单纯依赖模型自觉要稳得多。3. 第一个可复用技能的完整落地会议纪要整理器目标定好了结构也想清楚了接下来就是动手。第一个技能我选择做会议纪要整理器原因是它输入输出非常典型而且实用价值立竿见影。输入是一段带发言人标记的原始转录文本输出是结构化的纪要JSON。3.1 技能类的骨架设计让每个技能长成同一个模样为了让后面的技能都能套同一个模板我先定义了一个抽象的基类。基类里包含技能名称、描述、核心参数说明和运行方法。描述字段是给“技能发现”用的相当于给每个技能贴一个标签后面注册中心要靠它来做路由。from abc import ABC, abstractmethod from typing import Dict, Any class AISkill(ABC): 所有AI技能的基础抽象类 def __init__(self, name: str, description: str, version: str 1.0.0): self.name name self.description description self.version version abstractmethod def run(self, **kwargs) - Dict[str, Any]: 执行技能统一返回结构化字典 pass def validate_input(self, **kwargs) - None: 输入参数基础校验子类可重写 required getattr(self, required_params, []) for param in required: if param not in kwargs or kwargs[param] is None: raise ValueError(f缺少必填参数: {param})这个骨架看起来简单但作用非常关键。它给所有技能定死了统一的run方法签名调用方永远只需要关心一件事传进去一个字典收回来一个字典。至于技能内部是调用大模型、查数据库还是混合编排全都不影响外部。3.2 内部组成三件套系统提示词、示例注入、格式化输出基类有了下一步是填充具体技能的内部逻辑。我拆成了三个部分系统提示词负责定义角色和任务边界示例注入负责把输出格式“教”给模型这一步比任何描述都管用格式化与后处理负责把模型返回的内容变成真正的结构化数据。以下是我实现的会议纪要整理器的核心代码这段代码也是后面所有技能的模板import json import re from typing import Dict, Any, List class MeetingMinutesSkill(AISkill): def __init__(self): super().__init__( namemeeting_minutes, description将会议转录文本整理为结构化会议纪要, version1.0.0 ) self.required_params [transcript] self.SYSTEM_PROMPT 你是一名专业的会议纪要整理助手。请根据用户提供的会议转录文本提取关键信息并整理为结构化JSON。 要求 1. 准确识别会议主题、参会人、主要议题和结论 2. 每个议题下列出对应的行动项并标注负责人如果原文中能推断 3. 输出必须为合法的JSON对象不要包含任何其他文字 self.EXAMPLE 输入示例 张三今天讨论新功能上线计划。李四我觉得分两期走先上基础版本。王五那数据迁移怎么办张三下周一前我给出迁移方案。 输出示例 { meeting_topic: 新功能上线计划, participants: [张三, 李四, 王五], issues: [ { issue: 上线节奏, conclusion: 分两期上线先上基础版本, action_items: [] }, { issue: 数据迁移, conclusion: 需要给出迁移方案, action_items: [ {action: 给出数据迁移方案, owner: 张三, deadline: 下周一} ] } ], summary: 会议确定了分两期上线的策略数据迁移方案由张三负责 } def _build_prompt(self, transcript: str) - str: return f{self.SYSTEM_PROMPT}\n\n参考示例格式\n{self.EXAMPLE}\n\n待整理文本\n{transcript} def _parse_response(self, raw: str) - Dict[str, Any]: # 先尝试直接解析JSON try: return json.loads(raw) except json.JSONDecodeError: pass # 如果失败尝试从代码块中提取 pattern r(?:json)?\s*(.*?)\s* match re.search(pattern, raw, re.DOTALL) if match: try: return json.loads(match.group(1)) except json.JSONDecodeError: pass raise ValueError(模型输出无法解析为JSON) def run(self, **kwargs) - Dict[str, Any]: self.validate_input(**kwargs) transcript kwargs.get(transcript).strip() if not transcript: raise ValueError(转录文本不能为空) prompt self._build_prompt(transcript) raw_output self._call_model(prompt) # 调用大模型接口 parsed self._parse_response(raw_output) self._validate_output(parsed) return parsed def _validate_output(self, data: Dict[str, Any]) - None: # 基础结构校验 assert isinstance(data, dict), 输出必须是字典 assert meeting_topic in data, 缺少meeting_topic字段 assert isinstance(data.get(issues), list), issues字段必须是列表 # 更多字段可用JSON Schema校验这里从简代码里我故意保留了_call_model这个占位方法因为它对接的是具体的大模型服务商接口不同的团队可能用的不一样。但封装的思想是一致的模型调用的细节永远不让外面的代码看到。3.3 为什么我在提示词里放了一个完整示例而不是简单描述这里有个很重要的实操心得。很多人写提示词会写“请输出JSON格式”但实测效果非常不稳定。模型可能把JSON的值写成字符串也可能加上多余的解释文字。我加了完整示例之后输出合规率从大概六成直接拉升到九成以上。原因其实不难理解大模型在填空场景里表现最好给它一个标准答案的样式它更倾向于模仿而不是自由发挥。所以我在所有技能封装里都坚持一个原则能用示例就不要用描述能用填空就不要用提问。4. 稳定性不是靠运气解析、重试与边界处理第一个技能跑通之后我马上进入第四项工作稳定性加固。这一步做不做直接决定技能是只能在演示环境里跑还是真能上线被业务用。聊天可以容忍输出飘忽工具不行。4.1 模型回传的“小毛病”比想象中多说出来都不稀奇实测第一天晚上我整理了20条模型回传记录问题千奇百怪。有的是JSON里混了中文冒号有的是字段值里带换行导致解析炸掉有的是主题字段里居然带着“会议主题是”这样的前缀最离谱的是有一次模型把整个JSON放进了Markdown的表格里。这些问题的共性在于模型在“格式纪律”上远没有程序那么稳定。既然它不可靠那就要在封装层建立一个防线。我采用的策略是三层防线第一层是提示词示例尽可能精确第二层是解析器必须健壮能处理代码块包裹、BOM、多余空白等常见情况第三层是如果解析实在失败不要直接抛异常而是带上原始输出信息方便定位是模型问题还是提示词问题。4.2 重试策略别把所有不确定因素都交给模型重试是稳定性加固里最容易偷懒的部分。很多人一看到解析失败就无脑重试这其实是有代价的。每次重试都意味着一次新的模型调用耗时可能是一倍以上费用也是。所以我把重试分成两类一类是解析失败重试重试时会把上次的输出一并带回提示词里告诉模型“你的输出格式不合规请参考示例重新输出”另一类是内容缺失重试比如校验时发现缺少关键字段重试时把缺失字段单独强调一下。def run_with_retry(self, max_retries: int 3, **kwargs): last_error None for attempt in range(max_retries): try: result self.run(**kwargs) # 这里可以做业务级校验比如必填字段是否完整 self._validate_output(result) return result except Exception as e: last_error e # 在kwargs中注入上一次的输出帮助模型纠错 kwargs[_last_error] str(e) kwargs[_last_output] getattr(self, _last_raw_output, ) raise RuntimeError(f技能执行失败重试{max_retries}次仍失败: {last_error})这个方法实测下来最大的收益不是成功率提升而是让我敢把技能交给下游程序去自动调用了。因为在没有重试保护的版本里一个格式错误就足以让整条流水线中断而有了重试之后即使偶尔抽风也能自愈最多就是多花了点时间和token。4.3 边界条件清单空值、长文本、异常字符一个都不能放过除了模型本身的问题输入侧的边界条件同样容易踩坑。空文本、超长文本、全角半角混用的标点、敏感词这些如果不做处理后面全都会变成解析层的定时炸弹。我给技能封装修了一条默认的边界处理规则所有输入先strip文本长度超出上限就做截断并加标记所有输出字段做类型检查比如participants必须是数组不能是逗号分隔的字符串对可能有敏感信息的字段在返回前做一个简单的匹配检查宁可返回脱敏结果也不能把异常内容带出去。这些处理在代码里只占十几行但价值非常大。因为没有这些约束一个面向生产环境的技能就是一个会随时爆炸的黑洞你根本不知道它在哪一次调用时会吐出什么怪物。5. 从单兵技能到技能注册中心让技能可以被统一管理和调度第二个技能我做了“批量标题生成器”差不多半小时就完成了因为骨架完全复用。但做完之后我发现一个问题两个技能如果各管各的调用方每次都得自己记住该用哪个类这跟不封装也没太大区别。真正的复用需要有一套注册和发现机制。5.1 为什么需要注册中心名字即服务描述即路由技能注册中心的核心思路是让调用方只靠名字和参数就能使用技能而不需要关心具体的类。这有点像做饭时你只说要一份“番茄炒蛋”而不需要关心厨师用的是哪个锅、哪把铲子。实现上我把所有技能实例注册进一个字典再给每个技能挂一个简短的description字段。class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: AISkill): if skill.name in self._skills: raise KeyError(f技能已存在: {skill.name}) self._skills[skill.name] skill def unregister(self, name: str): self._skills.pop(name, None) def get(self, name: str) - AISkill: if name not in self._skills: raise KeyError(f未找到技能: {name}) return self._skills[name] def list(self): return [{name: s.name, description: s.description, version: s.version} for s in self._skills.values()] def execute(self, name: str, **kwargs): skill self.get(name) return skill.run_with_retry(**kwargs)有了注册中心之后调用方就变成了这样registry SkillRegistry() registry.register(MeetingMinutesSkill()) registry.register(TitleGeneratorSkill()) # 业务系统里一条代码就能调用 result registry.execute(meeting_minutes, transcript...)5.2 版本号和参数配置技能的运维思维单个技能可以随便改当技能多了以后就得考虑兼容性。我这次的版本还很原始只保留了一个version字段但我已经预留了一个重要的扩展点把提示词和示例从代码里抽出来变成可配置的资源文件。理由很简单业务方会经常调整话术、术语表、输出口径每一次都改代码发版本太慢了。更好的做法是技能从配置中心加载自己的提示词模板和示例这样运营人员直接改配置就能让技能“换脑子”。5.3 单一职责与组合使用技能不应该越做越重还有一点特别重要技能要尽量保持单一职责。不要试图做一个“万能整理技能”既干纪要又干翻译还干摘要。那样的话任何单一任务的提示词优化都会影响其他任务到最后整得哪个都弄不好。我第二天实际的做法是一个技能专注解决一类问题如果多个技能要协作比如先摘要再翻译那就让注册中心之上再加一个编排层。编排层只关心一个技能的输出如何变成另一个技能的输入完全不关心技能的内部实现。这个抽象在后续扩展中会非常舒服。6. 第二天实测避坑清单五个坑每个都让程序“死”过最后这部分写写实战中实打实踩过的坑。有些坑不踩根本不会想到但踩过一次之后就再也不想踩第二次了。6.1 坑一JSON解析死在中文标点上第一次跑通技能的时候我信心满满地把一段真实会议录音的转录文本丢进去。结果解析器直接报JSONDecodeError。打开原始输出一看模型把字段分隔符写成了中文全角冒号“”。这种问题出现的概率不高但一旦出现就是致命级。解决办法就是我在4.1里说的那套解析容错逻辑同时在提示词示例里刻意用普通冒号最后加上一句“所有标点必须使用英文半角”。6.2 坑二越长的上下文越容易“失忆”第二个技能我一开始是把整个对话历史都塞进上下文里的结果是输出质量反而变差。模型会被前面的闲聊带跑抓不住真正的任务指令。后来我把上下文策略改成了系统提示词永远是优先级最高的示例永远紧跟系统提示词用户真正要处理的内容放最后。这样一个“总-分-总”的结构模型执行效率明显高很多。6.3 坑三无脑重试把token费用翻了三倍这是我踩得最肉疼的一个坑。第一次加重试的时候我把超时和解析失败都做成最多重试5次结果某个请求因为模型服务商短时抖动一连重试了5次每次都是长文本输入费用直接翻了几倍。后来我学乖了重试次数降为3次并且每次重试之前先判断错误类型网络超时立即重试解析失败要带回错误信息再重试内容校验失败则通常不是重试能解决的直接抛异常。不同错误走不同的重试路径节省的token非常可观。6.4 坑四下游程序被空字段搞崩最早版本的技能不做字段校验只保证“能解析出JSON”。结果有一次模型把participants返回成了空数组下游的自动通知任务一拿到空数组就开始发错误告警。这让我意识到解析成功不等于字段完整字段完整不等于值合法。现在每个技能里都单独维护一个必填字段表和字段类型表解析之后先过Schema校验再返回。6.5 坑五技能描述写得太文艺导致自己都找不到技能在哪注册中心做好之后我往里面注册了3个技能。为了方便我把description写成了“帮助你快速生成好标题”“让纪要整理更高效”这类话。结果第二天想用的时候愣是要点开源码才能想起来哪个技能是干嘛的。后来我把描述规范改成动词对象可交付成果比如“根据会议转录文本生成结构化纪要JSON含议题与行动项”。这个格式一眼就能看懂也方便后续做能力发现。第二天实战下来我最深的体会是封装AI技能真正难的不是调用模型那一下而是它周围那圈看不见的工程细节。输入怎么定、输出怎么验、出错怎么办、多个技能怎么管每一个都想清楚了AI才能从一个“能聊天的东西”变成一个“能干活的东西”。下一步我打算做两件事一是把提示词配置化让业务方不碰代码也能调整技能行为二是尝试做一个简单的技能编排引擎让多个技能像流水线一样串联起来。如果你也在做类似的事情欢迎在评论区聊聊你封装时踩过的最坑的一个问题我保证下次继续实战的时候把大家的经验一起试一遍。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询