AI代理营销技能库实战:如何用结构化技能替代散装Prompt

发布时间:2026/10/13 23:26:24
AI代理营销技能库实战:如何用结构化技能替代散装Prompt 做AI代理有个很现实的问题你辛苦攒了十几个prompt模板结果代理一上线就四处漏风——有的不按格式输出有的把产品卖点写成说明书有的换了个场景就胡说八道。我上个月把团队在营销场景踩的坑汇总成了一个内部项目代号Marketingskills核心就一句话把散养的prompt变成一套有边界、可复用、能评估的营销技能库。今天这篇就聊聊这个项目的技术拆解和实操过程适合给AI代理设计技能体系的开发者以及想在营销场景落地大模型应用的内容团队参考。1. 项目整体设计与定位拆解1.1 为什么需要“营销技能库”而不是散装Prompt最早我们团队做营销内容生成习惯是每个人维护几个prompt文件互相复制粘贴谁需要谁就改两句。结果就是同一个商品文案A同事写的开头和B同事写的结尾完全不是一个语气甚至同一个prompt在两个业务线被改出十几个版本。更要命的是当AI代理需要在一个会话里连续完成“分析竞品 → 生成标题 → 提炼卖点 → 写正文”这一串动作时散装prompt根本管不住状态上一轮输出的字段下一轮就丢了。这就好比工具箱里所有螺丝都堆在一个抽屉里你要找一颗M4内六角得把整个抽屉倒出来翻。而我想要的是一面洞洞板每颗螺丝挂在固定位置贴好标签伸手就能拿用完了还知道放回去。营销技能库就是这块洞洞板每一个技能都有明确的输入输出边界、独立的执行逻辑和可重复调用的接口。AI代理不再靠“猜”来组合prompt而是通过技能注册表查找、匹配、调用像软件工程一样管理营销内容生成逻辑。为什么散装prompt撑不住AI代理原因有三个第一意图识别不稳定。没有统一的技能清单时代理只能靠LLM自己“揣测”该干什么经常是问它“写个朋友圈推广”它先给你讲了一通营销学理论而不是直接输出文案。第二上下文污染严重。多个prompt共享同一个系统提示用来约束风格的句子可能被另一个技能误读成事实导致输出内容里混入莫名其妙的限定条件。第三无法量化质量。散装prompt的好坏全凭感觉没法建立回归测试集改一版prompt可能会影响所有下游任务但没人知道。所以我做Marketingskills时的第一个决定就是所有营销能力必须以“技能”为单位做结构化建模每个技能有名字、有描述、有输入schema、有输出schema、有执行函数从源头掐死无边界复用。1.2 技能库的五大核心模块整个Marketingskills项目没有起高超的架构就五个模块各干各的事模块职责关键产出技能注册表存储所有技能定义支持查询、校验、版本管理技能Schema、技能清单执行引擎根据代理意图匹配技能调度LLM或外部工具调用日志、执行结果上下文管理器维护会话状态、品牌知识、历史消息统一注入技能结构化Context对象评估管线离线跑测试集统计格式合规率、营销质量分回归报告、评估指标权限与审计控制哪些代理可用哪些技能记录调用链权限策略、Trace ID这五个模块里最容易被忽略的是“上下文管理器”。我刚开始以为只要把技能定义好执行时把历史消息全塞进去就行结果代理经常把上一轮生成的“竞品分析”当成下一轮的“品牌卖点”来用。后来我规定上下文管理器只向技能注入该技能声明过的白名单字段比如生成文案技能只需要用户意图、商品名、卖点列表、品牌语气其他历史一律不传。这个约束让技能之间的数据流变得可控也大大降低了代理“幻觉”的概率。另一个关键点是权限与审计。营销场景里有的技能面向全员开放比如生成slogan有的只能给特定团队用比如基于投放数据做预算分配建议。权限控制的实现很简单技能定义里加一个“allowed_roles”字段代理在调用前先校验角色。审计方面我要求每次技能调用都要记录trace_id这样后续排查“这条文案为什么有这个奇怪句子”时能从日志反查到是哪次输入、哪条prompt、哪个模型参数产生的。1.3 技术选型自研还是复用框架项目启动时团队里有人建议直接用某开源Agent编排框架理由是生态成熟、代码少写一半。我纠结了一周最后决定框架可以借用但核心的注册表和执行链路必须自研。原因特别朴素。开源Agent框架擅长的是通用任务编排比如“规划步骤 → 调工具 → 总结答案”。但营销技能库的特殊性在于它的输出要过“双重校验”先是格式校验必须符合JSON Schema然后是业务校验必须符合品牌调性、不能有违禁词、不能过度承诺。通用框架通常只做前者甚至连前者都做得不够严格它们倾向于把结果直接丢给下一环节。我最终采用的路子是基于一个极轻量的并发调度库只用了它的任务队列和超时控制自己写技能注册表和匹配逻辑。这个取舍让我在最难调的“LLM选技能”环节保住了控制权。选技能我用的是“语义匹配规则兜底”先把用户意图做词法分析找出候选技能再把技能描述和用户请求一起丢给LLM做最终选择。如果LLM返回的技能名不存在注册表就自动触发兜底策略调用一个“通用对话”技能而不是让代理直接报错。2. 技能定义与实现细节解析2.1 技能描述的结构化Schema技能定义是整个项目的基石。我参照OpenAPI规范做了简化每个技能必须包含五个字段name、description、parameters_schema、output_schema、handler。下面是一个生成商品详情页文案的简化示例{ name: generate_product_copy, description: 生成商品详情页文案适合电商产品推广场景。输入商品名称和核心卖点输出标题、正文、行动号召。, parameters: { type: object, properties: { product_name: { type: string, description: 商品名称例如『无线降噪耳机Pro』 }, selling_points: { type: array, items: {type: string}, description: 核心卖点列表每一项是一个具体功能点 }, target_audience: { type: string, description: 目标人群例如『通勤上班族』 } }, required: [product_name, selling_points] }, output: { type: object, properties: { headline: {type: string, description: 一句话主标题}, body: {type: string, description: 正文文案300字以内}, cta: {type: string, description: 行动号召文案} } } }这里有两个容易踩坑的点。第一个是description必须写“什么时候用、输入是什么、给什么东西”而不是写“这是一个生成文案的技能”。因为LLM在选择技能时靠的是语义匹配描述越具体匹配准确率越高。我试过把description写成“用于生成商品文案”结果代理在写活动邀请函时也选中了它后面加了“适合电商产品推广场景”才明显好转。第二个是参数schema不要写得太死。早期我把“target_audience”设为required但如果代理没拿到人群信息就会自己在参数里编一个“大众消费者”反而误导文案。后来我把非关键参数全部设为optional并允许参数值为空交给技能内部的prompt去决定怎么处理。记住schema是给模型看的边界不是给业务方的需求文档留一点弹性空间。2.2 技能编排与上下文传递机制技能编排听起来玄乎其实就是解决三个问题选哪个技能、参数从哪来、上下文给什么。选技能这块我采用两阶段策略。第一阶段是粗筛用正则匹配用户请求里的关键词比如“文案”“详情页”“朋友圈”这些词命中后生成候选技能列表。第二阶段是精排把候选技能的名称和描述拼成一段文本连同用户请求一起发给LLM让LLM返回一个JSON包含技能名和参数字典。{ tool_choice: generate_product_copy, arguments: { product_name: 无线降噪耳机Pro, selling_points: [主动降噪, 30小时续航, 轻若无物] } }这里有个细节LLM返回的arguments经常把数组类型写成逗号分隔的字符串这需要在校验层做类型归一。我写了一个preprocess函数对每个字段按照schema里的类型做转换。上下文传递机制是另一个让团队受益很大的设计。我定义了一个Context类dataclass class Context: session_id: str user_intent: str brand: str | None None product_info: dict field(default_factorydict) brand_voice: str 专业、简洁、有温度 memory: list[dict] field(default_factorylist)在执行技能前上下文管理器会从全局会话中抽取与当前技能相关的字段塞进Context再传给技能的handler。每个技能的handler只接收这个Context不能直接访问全局变量。这样做的收益是所有技能的可测试性大幅提升我可以直接构造一个Context来单测技能不用去mock整个会话。2.3 营销技能的质量评估维度技能库上线前需要回答一个核心问题怎么证明这个技能比散装prompt好我建立了三条评估维度缺一不可。第一格式合规率。这是硬指标必须是100%。技能output_schema定义了字段名、类型、必填项只要有一个字段缺失或类型错误整个调用就算失败。我写了一个校验器把模型输出解析成JSON后逐个字段比对不符合就触发重试。第二营销有效度。文本生成类技能很难自动化判断我采用“人工评分抽样AB”的方式。具体做法是每次评估在测试集里随机选20条请求让三个业务人员从“卖点清晰度”“情感感染力”“行动引导力”三个角度打分取平均值作为该技能的得分基线。注意这个基线必须和线上实际效果分开看线下模拟永远替代不了真实投放。第三品牌一致度。营销内容的红线是不能说违禁词、不能过度承诺、不能偏离品牌语气。我用两种手段静态词表过滤、动态风格向量对比。静态词表维护了“最先进”“百分百”“国家级”这类词一旦命中就标记警告。风格向量对比是把生成文案与品牌历史优秀文案的embedding做余弦相似度低于阈值则判定为“风格漂移”打回重写。下面是我用表格维护的评估看板示例技能名格式合规率营销有效分品牌一致分平均耗时generate_product_copy100%4.2/50.912.1sgenerate_social_post100%3.8/50.871.8sanalyze_competitor100%4.0/50.903.5s形成这个看板之后每次修改技能内部prompt我都能直观看到哪些指标下降了而不是凭感觉说“这次改得好像更顺了”。3. 实操过程从零搭建营销技能库3.1 环境准备与基础依赖这个项目的代码量不大但需要的基础依赖比较明确。我用的是Python 3.11依赖库包括一个本地小工具库用于JSON解析和schema校验一个LLM客户端SDK底层接入统一的模型网关以及一个轻量并发工具用于处理技能超时。这里不贴具体安装命令因为模型网关和SDK不同项目差异很大。说一个辅助工具的选择所有LLM客户端调用我都封装成同一个接口方便切换不同模型。技能内部不直接调用模型SDK而是通过一个llm_client.chat(messages, temperature0.7)的接口这样测试时可以用mock替换线上也可以给不同技能配置不同的temperature。3.2 核心代码技能注册与调度技能注册表的实现很直接我用字典存储技能实例注册时校验重名和schema合法性。# skill_registry.py import json from dataclasses import dataclass, field from typing import Callable, Any dataclass class Skill: name: str description: str parameters_schema: dict output_schema: dict handler: Callable[[dict, Context], dict] class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(f技能重名: {skill.name}) # 简单校验schema assert type in skill.parameters_schema, 参数schema必须指定type assert properties in skill.parameters_schema, 参数schema必须包含properties self._skills[skill.name] skill def get(self, name: str): return self._skills.get(name) def list_skills(self): return [{name: s.name, description: s.description} for s in self._skills.values()] def validate_arguments(self, name: str, arguments: dict): 用参数schema校验并归一化类型 schema self._skills[name].parameters_schema required schema.get(required, []) for r in required: if r not in arguments: raise ValueError(f缺少必填参数: {r}) # 这里省略完整jsonschema校验实现 return arguments技能调度器是核心中的核心。它接收用户的原始请求先粗筛候选技能再让LLM做选择然后执行并校验输出。# dispatcher.py from skill_registry import SkillRegistry, Skill class Dispatcher: def __init__(self, registry: SkillRegistry, llm_client): self.registry registry self.llm llm_client def _candidate_skills(self, user_request: str): # 粗筛按关键词命中 candidates [] keyword_map { 文案: [generate_product_copy, generate_social_post], 详情页: [generate_product_copy], 竞品: [analyze_competitor], 朋友圈: [generate_social_post], } for keyword, skill_names in keyword_map.items(): if keyword in user_request: candidates.extend(skill_names) return list(set(candidates)) or [generic_response] def dispatch(self, user_request: str, context: Context): candidates self._candidate_skills(user_request) # 精排让LLM从候选列表里选一个并提取参数 skill_descriptions [ f{name}: {self.registry.get(name).description} for name in candidates if self.registry.get(name) ] prompt f根据用户请求从以下技能中选一个最合适的输出JSON{{skill: 技能名, arguments: {{...}}}}。 用户请求: {user_request} 技能列表: {, .join(skill_descriptions)} 只输出JSON。 raw self.llm.chat([{role: user, content: prompt}], temperature0.0) parsed json.loads(raw) skill_name parsed.get(skill) if skill_name not in candidates: skill_name generic_response skill self.registry.get(skill_name) args self.registry.validate_arguments(skill_name, parsed.get(arguments, {})) result skill.handler(args, context) # 输出校验这里简化只做JSON类型校验 return {skill: skill_name, result: result}这段代码看起来简单但有几个细节值得说。_candidate_skills里的关键词映射需要持续维护。我每遇到一次误选就把用户请求里的新词补进去。比如刚开始没有“详情页”关键词代理把“写详情页”请求转发给了generic_response后来加上这个词误选率立刻下降。LLM选技能时的temperature必须设为0或接近0因为它不是在创作而是在做分类任务。调高temperature会导致技能名出现拼写变体比如返回“generate_productcopy”白白增加容错逻辑。3.3 接入AI代理的完整链路技能库不和具体的代理实现绑定它提供了一套执行接口。完整的链路是这样的代理收到用户消息把它交给DispatcherDispatcher调用上下文管理器构建本次会话的Context进行技能匹配执行技能技能handler返回结构化的result代理把result渲染成自然语言回复。关键是第5步。很多代理失败是因为让同一个模型既选技能、又生成回复导致模型把技能调用的JSON混在回话里。我采用“两段式生成”第一段只做工具调用temperature0第二段基于工具结果生成文案temperature0.8。这样既保证了工具调用稳定又不牺牲生成文案的多样性。下面是一个实际调用后的日志片段[12:00:01] 收到请求: 给这款耳机写个详情页文案 [12:00:02] 粗筛候选: [generate_product_copy, generic_response] [12:00:03] LLM选择: generate_product_copy [12:00:05] 参数校验通过: product_name无线降噪耳机Pro, selling_points... [12:00:06] 调用handler注入Context包含品牌语气和历史消息 [12:00:08] 输出校验通过生成result [12:00:09] 代理渲染最终回复这段日志每行都对应一个真实环节。如果后续出了线上问题我可以快速定位是匹配错、参数错、模型输出错还是渲染错。3.4 效果验证与迭代方法技能库和模型本身一样需要持续迭代。我搭建了一个离线回归脚本每次修改技能的prompt或schema都跑一遍固定测试集输出合规率和人工评分结果。这个脚本是整个项目的定心丸。测试集维护了三档请求第一档是高频典型场景15条比如“生成详情页文案”“写朋友圈推广”第二档是边界场景10条比如“没有卖点信息”“用户要求写违规词”第三档是干扰场景5条比如“问天气但提到文案”。每一档都有对应的期望输出类型和必须满足的约束条件。回归脚本的核心逻辑很简单逐条调用Dispatcher把返回的result和期望schema做比对生成通过/失败列表。人工评分每周做一次抽最近新增或修改过的技能来评。这样做两周后我发现一个明显规律内部prompt里例子越多营销有效分越高但响应耗时也会上升。于是我给每个技能的prompt设置了“最多示例不超过3条”的限制把省下来的时间留给了第二段生成。4. 常见问题与排查技巧实录4.1 技能之间互相干扰命名空间与权限隔离项目运行一周后出现了一个诡异现象调用“generate_social_post”生成朋友圈文案时开头总会冒出“详情页主标题”这样的词。查日志发现是因为两个技能的handler在底层复用了一个公共prompt拼接函数函数里从某个全局字典读取“最近一次生成的标题”结果被另一个技能写入了。解决办法是强制技能间隔离所有handler只允许访问自己schema里声明的参数以及Context中明确提供的字段。我写了一个with_isolation装饰器动态检查handler里用到的全局变量访问发现跨技能引用就报错。同时技能之间的prompt模板必须放在独立的文件或命名空间里不允许使用一个global的prompt变量。4.2 模型输出不遵循技能格式的兜底策略再怎么prompt模型偶尔还是会返回带Markdown的JSON、缺少结束括号的JSON或者把日期格式写成“2024-5-1”而不是要求的“2024-05-01”。我设计了三层兜底。第一层是解析容错。先把模型输出里的“json”和“”去掉再把多余的解释文本截掉。我封装了一个safe_parse_json函数尝试多种策略提取最可能的JSON片段。第二层是信息保留。如果解析失败程序不会直接报错而是把原始输出当作“body”字段塞进result并标记_parse_failed: true让上层决定是否重试还是返回给用户。第三层是二次生成。对于关键技能我会把schema要求重新发给模型让模型严格按照JSON格式重新输出同时附带第一次的输出作为参考。实测下来二次生成的成功率在80%左右。这里有一个教训不要无限重试。我最初设置了最多3次重试但有一次模型持续烧钱3次都失败也没能救回来。建议最多重试1次失败后走降级方案直接调用一个固定模板技能兜底。4.3 营销内容风格漂移如何固定品牌语气风格漂移是最玄学的问题。同一个技能上午生成的文案还有品牌调性下午就变成产品说明书的语气数据一查发现是上下文管理器注入的品牌语气字段被某次会话污染了。我的解决方法是“语气锚点”。每个品牌在Context里有一段固定的brand_voice描述但这段描述太长会占token太短又不够约束。我把它做成两段式一段是简洁的风格指令比如“语气专业但不冷硬多使用动词少用形容词”另一段是3条品牌历史优秀文案示例存放向量数据库里。技能需要时通过embedding检索最相关的2条示例放进上下文中。这个方案的本质是把品牌语气从“一次性写死在prompt里”变成“可检索可更新的资产”。每次品牌调性调整只需要更新向量库里的语料所有技能自动生效不用再逐个改prompt。4.4 排查技能调用失败的工具化手段当线上代理出现人工反馈“文案乱写”时我需要快速判断是哪个环节出了问题。我给Dispatcher加了一个装饰器记录每个技能调用的完整链路。这个装饰器做的事情很简单在技能调用前记录输入调用后记录输出和耗时如果抛异常就记录堆栈。import functools, time, traceback def trace_skill_call(func): functools.wraps(func) def wrapper(*args, **kwargs): trace_id kwargs.get(trace_id, unknown) start time.time() try: result func(*args, **kwargs) logger.info({ event: skill_call_success, trace_id: trace_id, skill: func.__name__, duration_ms: round((time.time() - start) * 1000, 2), result: result }) return result except Exception as e: logger.error({ event: skill_call_failed, trace_id: trace_id, skill: func.__name__, error: str(e), stack: traceback.format_exc() }) raise return wrapper给每个技能handler都加上这个装饰器配合trace_id就可以在日志系统里按trace_id聚合所有相关日志看到“Agent收到请求 → 调度器选择技能 → 调用handler → 输出校验”的全过程。这个能力在项目上线初期帮了大忙很多“症状”看着像模型幻觉实际是参数校验时类型转换bug导致的。5. 后续扩展与个人实际体会5.1 技能库向插件生态演进的思考Marketingskills目前只服务内部营销团队但我在设计之初就有意把注册表接口做得足够通用期望未来可以像插件市场一样开放给更多团队。具体设想是每个技能包可以是一个目录里面包含schema.json和handler.py由其他团队打包上传到内部仓库注册表动态加载。不过要提醒的是插件化会增加安全和审核成本。技能是执行任意代码的一旦开放就要做沙箱隔离、资源限制和调用审计。我的建议是先保持单体仓库模式把技能库内部模块之间通过接口解耦但物理部署暂时不要拆开。等技能数量超过50个、团队超过3个时再考虑拆成独立服务。过早微服务化只会让你陷入分布式调试的泥潭营销场景的核心矛盾是内容质量和调用稳定不是并发规模。5.2 踩坑后的几点心得项目做到现在我最大的体会是AI代理技能库本质上是用工程方法管住模型的自由度。模型本身有很强的表达能力但营销场景需要的是稳定、可控、可复用的输出而不是每次都有惊喜。如果你也要做类似的东西我有几条比较实在的建议第一技能描述要像程序员写接口一样严谨。不要把“一点小细节”留给模型自己悟它真的会悟出奇怪的东西。第二不要一开始就把技能库设计得太宏大。我最初想支持技能依赖图、并行调度、多模型路由后来全砍了只留线性调用。实际上营销代理的任务链大多是串行先分析、再生成、最后优化极少需要并行。砍掉复杂功能后系统稳定性反而上来了。第三建立黄金测试集比什么都重要。一个没有回归测试的技能库就是一颗定时炸弹。你无法感知某次prompt微调对全局的影响等用户投诉才发现就晚了。第四版本管理不要只盯代码技能的prompt也要版本化。我把每个技能目录里放一个prompt_v2.txt同时在注册表里记录当前激活版本改坏了可以秒级回滚。现在这个项目还在持续迭代中下一步打算给技能库加一个“技能自动组合”的能力让代理在完成复杂任务时可以像搭积木一样把多个技能串起来。但那还需要更细粒度的技能依赖描述和更强的规划能力等真正落地了我再回来聊。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询