ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Coze技能开发实战:从Prompt模板到部署调试全攻略

Coze技能开发实战:从Prompt模板到部署调试全攻略

在Coze这类大模型应用工作台上折腾过一阵子的人,基本都会遇到同一个坎:想让Agent稳定完成一件具体的事,靠那段冗长的system prompt不断堆约束,效果越堆越飘,模型偶尔还是会答非所问。后来接触到Skill(技能)这个概念,才意识到整个写Prompt的思路都要变——不是把大模型当成一个听指令的闲人,而是给工作台装上一个有明确输入输出规格的“功能件”。这篇文章我打算把Coze技能开发中最容易被忽略、也最决定成败的那部分——Skill的Prompt撰写,从模板讲到落地,整理成一条可以直接照做的路径。无论你是刚入门想给Bot加个查询技能,还是已经在维护好几个技能包想统一规范,这篇都能给你一套可复用的思路。

1. 弄懂Coze Skill的组成,才能写好那个Prompt

很多人上来就打开编辑器写Prompt,写完塞进去发现技能调用时经常“不在状态”,要么不触发,要么参数取错。问题往往出在没搞清技能包里到底有几样东西,以及Prompt在其中扮演什么角色。

1.1 Skill不是“一个提示词”:先看技能包里的三个部件

一个可部署的Skill,我习惯叫它“技能包”,因为它从来不是单一的一段文字,而是由多个部件组成的项目结构。理解这个结构是写好Prompt的前提。

技能包里至少有三个核心部件:

部件作用是否必须
技能描述与配置信息定义技能名称、版本、功能描述、触发条件,注册到平台上供模型识别必须
指令文件技能的Prompt本体,用自然语言描述角色、任务、输入、输出、处理流程必须
资源文件外部依赖的参考数据、代码片段、文档、工具调用说明按需

很多人只盯着中间那个指令文件写,把配置信息当回事但没当重点。实际踩坑之后你会发现,配置信息里的技能描述写得好不好,直接影响模型能不能在合适的时机把技能调起来。指令文件写得好,技能调起来了才能做对事;配置信息写得好,技能才能被“调起来”。这两者缺一不可。

打个比方,如果把Agent比作一家餐厅的店员,技能描述相当于菜单上的菜名和简介,客人(用户)看了才知道点什么,店员(模型)看了才知道推荐什么;指令文件则是后厨的做菜流程,蛋炒饭是先炒蛋还是先炒饭,得写清楚。菜单上写着“蛋炒饭”,后厨却按“蒸米饭”来操作,那端上来的东西肯定不对。

1.2 Prompt在技能包中的位置:从对话模板到指令文件的映射

Skill这套机制的本质,是把过去那些堆在system prompt里的“长尾指令”拆出来,做成一个可独立复用、可测试、可版本化管理的小模块。模型在主流程里什么时候决定调用这个模块?主要取决于两点:技能描述和参数定义。而模块被调用之后怎么执行?看的是指令文件里的Prompt。

所以你的Prompt其实承担了两个层面的工作:

第一层是“入口层”,写在技能描述里。它要做的是让模型能回答三个问题:这个技能是干嘛的?什么场景下应该用?用的时候需要用户提供哪些信息?描述越准确,模型越不容易把技能误用或漏用。

第二层是“执行层”,写在指令文件里。它要做的是约束模型:拿到参数后,先做什么、再做什么,中间有哪些判断分支,最后输出什么格式的结果。

很多人把这两个层次混在一起,技能描述写成“一个通用助手”,结果模型什么场景都想调用;指令文件又写得过于笼统,模型调用之后自由发挥。搞清楚这两层的分工,写Prompt的思路就清晰了。

1.3 模板化的价值:为什么“标准格式”能省掉80%的调试时间

既然每个技能都要同时维护入口层和执行层,那就自然引出一个问题:能不能把结构固定下来,按模板填内容?我的答案是必须能,而且应该这么做。

模板化至少带来四个直接好处:

  • 可扫描。平台解析技能配置时,通常会读取固定字段,字段名规范后不容易报错。
  • 可测试。每个技能的结构一致,测试用例可以批量设计,不用每次重新读一遍Prompt才知道怎么测。
  • 可复用。同类技能的指令文件可以快速复制改造,比如做一个“查天气”技能,再做一个“查汇率”,改参数描述和处理步骤就行。
  • 可版本对比。结构一致的情况下,diff两个版本的差异非常清楚,哪次改动引入问题一眼就能定位。

有人担心模板化会不会限制表达,导致Prompt僵硬。实际恰恰相反,模板只是外壳,里面的角色设定、步骤拆解、边界处理全是自由发挥的空间。把字段位置固定,反而让你有更多精力去打磨内容质量,而不是每次重新设计结构。

2. 核心Prompt撰写要点:把一句话讲成一套可执行流程

明确了Skill的结构之后,接下来进入正题:怎么写那段决定技能质量的Prompt。我见过不少开发者写Prompt特别“大而全”,面面俱到,但模型执行起来依然潦草。原因在于Prompt缺少“流程感”,只是一堆要求,不是一套可执行的操作逻辑。

2.1 角色设定与任务边界:先回答“你是谁”“你能做什么”

指令文件开头第一件事,不是急着列步骤,而是明确角色和职责边界。模型在技能执行时状态是独立的,它并不知道自己在整个Agent里担当什么,需要你在Prompt里重新给它一个“岗位说明”。

我通常这样写:

# 角色 你是一个【信息抽取与结构化输出】助手,仅负责处理与【XX业务】相关的查询。 # 任务边界 - 只处理输入参数中出现的业务内容,不回答无关问题。 - 不进行任何推测,缺失字段时必须明确提示。 - 不修改外部系统状态,只负责生成结构化结果。

这段看起来很朴素,但非常关键。它告诉模型三个信息:你是谁、你能做什么、你不能做什么。特别是“不做什么”,很多人会忽略。模型没有边界感的时候,经常会做出超出预期的行为,比如你让它根据参数生成一个查询条件,它顺手把结果也编出来了。

任务边界的设计有个技巧:与其抽象地写“不能编造信息”,不如具体列场景。比如“当参数中没有指定城市时,不要尝试补全城市名,直接返回参数缺失错误”。具体场景越多,模型踩线的概率越低。

2.2 输入描述与参数声明:让大模型知道要什么

Skill和普通对话Prompt最大的区别之一,就是它有一套参数体系。你在配置里声明参数列表,用户对话时模型负责从对话中抽取参数值,然后传给技能。所以参数声明绝不是随便写写,它决定了模型能不能准确理解用户意图。

参数声明通常包含这些字段:

字段含义写法建议
名称参数标识符,代码里调用时用统一小驼峰,如 cityName
类型string、integer、number、boolean、array等按实际业务选,拿不准用string
描述给模型看的说明,决定抽取准确率写清楚边界,如“用户要查询的城市名称,如北京、上海,不包括省份”
必填是否必须提供非必填的参数务必写清楚缺省逻辑
枚举值可枚举时给出候选列表能列就列,能显著降低模型自由发挥的概率

踩坑最多的就是“描述”。有人写“用户想查的城市”,模型抽取时很容易把“我昨天去过的那座城市”这种表述也解析成城市名。更好的写法是“目标城市的官方中文名称,必须是用户在对话中明确提到的地名;若用户未提及,则标记为缺失”。描述越具体,抽取越准。

另外,非必填参数一定要“留后路”。我见过一个技能,参数里有个可选的排序方式,结果模型有时候传“asc”,有时候传“升序”,代码直接裂开。后来我在描述里加上“只能传asc或desc,若用户未指定则传asc”,世界清静了。

2.3 处理步骤与输出格式:从“自由发挥”到“固定协议”

这是Prompt的核心段落。你要把任务的执行过程拆解成清晰的步骤,并且规定每一步的产物。模型只有在明确的流程里才能稳定输出,否则它会跳步骤、改逻辑。

我习惯用有序列表来写处理步骤,每步只干一件事:

# 处理步骤 1. 检查输入参数是否齐全,若有必填参数缺失,直接进入错误处理流程。 2. 根据参数中的用户ID,在会话上下文中查找是否已存在用户画像。 3. 若存在用户画像,结合画像与本次输入生成个性化结果。 4. 若不存在用户画像,仅基于本次输入生成通用结果,并追加提示语。 5. 将结果整理为指定JSON结构并返回。

每步后面最好跟一句说明,解释这一步的意图和边界。比如第2步可以追加“用户画像查找仅限于上下文窗口内,不要自行推测用户偏好”。这样模型就不会自作主张。

输出格式方面,建议直接给出模板,而不是描述。模型对“结构化输出”理解的可靠程度,远没有对“严格按照以下格式回填”高。举个例子:

# 输出格式 严格按照以下JSON结构返回,不要添加任何额外字段或说明文字: { "recommendReason": "结果生成依据,一句话概述", "recommendList": [ { "itemId": "内容ID", "title": "内容标题", "score": 0.9 } ], "missingParamHint": "若有缺失参数,提示用户补充;无缺失则返回null" }

这里重点在“不要添加任何额外字段”。模型经常会在JSON外加一层“```json”代码块标记,或者多输出一段解释文本。如果你要对接API或插件,这些额外内容全是脏数据。在Prompt里明确禁止,比程序里做各种容错要省事得多。

2.4 示例与边界处理:Few-shot和拒绝逻辑

光有步骤还不太够,模型对“抽象规则”的遵循能力有限,给它一两个正反示例,效果立竿见影。这就是Few-shot的作用。

正例要覆盖标准场景,反例要覆盖易错场景。我通常会写两组:

# 示例 示例1(标准场景): 用户输入:帮我把李白的一句诗翻译成英文 模型输出: { "taskType": "translate", "sourceText": "李白的一句诗", "targetLanguage": "english", "errorHint": null } 示例2(参数缺失场景): 用户输入:帮我翻译成英文 模型输出: { "taskType": "translate", "sourceText": null, "targetLanguage": "english", "errorHint": "请提供需要翻译的源文本内容" }

示例不用多,每个场景一个就够。关键是示例要和你定义的输入输出协议严格一致,不然模型会困惑到底参考哪条。你甚至可以在示例里把“缺失时应该怎么做”演一遍,让模型学到这个反射。

边界处理方面,要主动定义“模型不认识的输入”该怎么回应。很多人只写了正常流程,没写异常流程,结果模型遇到覆盖不了的输入时就开始编。我会专门加一段:

# 无法处理的情况 若输入内容超出技能定义范围,或缺少处理所需的关键参数,直接返回: { "errorCode": 40001, "errorMessage": "无法处理该请求,请检查参数是否完整", "data": null }

这段的作用是给模型一条“安全出口”,让它知道遇到意外时不是继续硬着头皮生成,而是规范地拒绝。反正模型无论如何都要输出点内容,不如引导它输出一个你能识别的错误结构。

3. 从模板到落地:写出一份可直接部署的技能包

写Prompt只是前半程,真正让技能跑起来,还要把它组装成技能包并完成部署。这一步很多人栽在“文件结构不对”“字段解析失败”“版本没更新导致改了无效”这些细节上。

3.1 搭建最小技能包:目录结构、文件命名与版本约定

先给一个我日常用的目录结构,适用性很广,你拿到手可以直接套:

my_skill/ ├── manifest.json # 技能元信息:描述、版本、触发条件 ├── instruction.md # 指令文件:Prompt本体 ├── reference/ │ ├── example_1.md # 可选的参考示例 │ └── api_notes.md # 可选的接口说明 └── assets/ └── data.json # 可选的静态资源

manifest.json是平台识别技能包的入口,字段要规范。我常用的一套字段大致这样:

{ "name": "book_query_skill", "version": "1.0.0", "description": "根据用户提供的书名或作者,返回书籍的基本信息、简介和评分。仅用于图书相关的查询。", "author": "dev", "license": "MIT" }

这里有几个细节:

  • name字段用英文小写加下划线,别用中文,也别用连字符,很多平台对name的格式有约束。
  • description是给模型看的第一手信息,它决定什么时候调用技能。开头写“根据用户提供的书名或作者,返回书籍信息”比写“一个图书查询助手”更利于触发。
  • version字段建议用语义化版本号,改动量小时升小版本,结构调整升大版本。

文件名方面,instruction.md这个名字我用得最多,原因是很多平台约定俗成默认读取这个文件。如果平台支持自定义,那也建议统一命名,方便自己维护。

3.2 写指令文件的实操过程:参数说明、返回值说明

把整个指令文件的骨架搭出来,你往里填内容就行。我习惯按这个顺序组织:

# 基本信息 技能名称:book_query_skill 技能描述:根据用户提供的书名或作者,返回书籍信息 # 角色定义 (见2.1) # 输入参数 | 参数名 | 类型 | 必填 | 描述 | | --- | --- | --- | --- | | bookName | string | 否 | 完整的书籍名称,须为用户明确提到的书名 | | authorName | string | 否 | 作者的完整姓名,须为用户明确提到的作者 | | category | string | 否 | 图书分类,可选值为 fiction / non_fiction / unknown | # 参数约束 - bookName 和 authorName 至少填一个,否则进入错误处理。 - category 未指定时,默认传 unknown。 # 处理流程 1. 检查参数。若 bookName 和 authorName 都为空,返回错误。 2. 根据 bookName 或 authorName 匹配本地资源。 3. 匹配成功,返回书籍的标题、作者、简介、分类。 4. 匹配失败,返回未找到的提示。 # 输出格式 { "matched": true, "data": { "title": "书名", "author": "作者", "summary": "简介", "category": "分类" }, "message": "处理结果说明" } # 示例 (示例内容按2.4方式补充) # 错误处理 参数均缺失时返回: { "matched": false, "data": null, "message": "请提供书名或作者信息" }

中间“输入参数”表格里写的描述很重要,模型抽参时以它为唯一依据。记得在“参数约束”里写清楚参数之间的关系,比如“至少填一个”,不然模型可能两个都给你留空。

返回值说明也别含糊。我见过有人只在输出格式里放了JSON示例,没做字段含义解释,结果模型改写了字段名。建议在输出格式后面补一段字段说明,把每个字段允许的取值写清楚。比如“matched字段只允许true或false”,这能极大减少模型乱赋值的概率。

3.3 一键生成与本地校验:利用模板生成、检查字段完整性

前面把模板定好了,接下来就可以考虑“一键生成”这件事了。所谓一键生成,不是真的凭空生成,而是把模板脚本化:写一个脚本,读配置,填充指令文件,输出完整技能包。这样每次新建技能都是稳定结构,不会手写出错。

我自己常用的思路是用一个简单的Python脚本维护一个技能清单,然后批量生成技能包。你可以按自己的实际情况调整:

skills = [ { "name": "book_query_skill", "version": "1.0.0", "description": "根据书名或作者查询书籍信息", "input_params": [ { "name": "bookName", "type": "string", "required": False, "desc": "完整的书籍名称" }, { "name": "authorName", "type": "string", "required": False, "desc": "作者的完整姓名" } ], "output_schema": '{"matched": true, "data": {"title": "书名"}}' } ] def generate_instruction(skill): params_table = "\n".join( f"| {p['name']} | {p['type']} | {'是' if p['required'] else '否'} | {p['desc']} |" for p in skill["input_params"] ) instruction = f"""# 基本信息\n技能名称:{skill['name']}\n技能描述:{skill['description']}\n\n# 输入参数\n| 参数名 | 类型 | 必填 | 描述 |\n| --- | --- | --- | --- |\n{params_table}\n\n# 处理流程\n(按需填写)\n\n# 输出格式\n{skill['output_schema']}\n""" return instruction for skill in skills: text = generate_instruction(skill) with open(f"generated/{skill['name']}_instruction.md", "w", encoding="utf-8") as f: f.write(text) print(f"generated: {skill['name']}")

这个脚本只是给你一个方向。实际用的时候,你还可以加一个校验函数:检查必填字段是否齐全、检查参数名是否符合命名规则、检查JSON示例能否被解析。把这些校验做成脚本的一部分,每次生成后自动跑一遍,能提前发现一大部分低级错误。

校验这一步特别建议做。因为平台导入技能包时,如果字段格式不对,经常只给一个模糊的报错,等你一层层排查黄花菜都凉了。脚本里校验好,至少能把问题锁在生成阶段。

3.4 在平台中调试和发布:测试、日志、版本迭代

技能包文件准备好之后,就是导入平台调试。我说几个实际发布流程里容易忽略的点。

先建测试用例。别上来就拿真实用户对话测,最好准备一组标准输入,覆盖正常场景、参数缺失、边界场景。比如上面图书查询技能,至少要测四类:只传书名、只传作者、书名作者都传、啥都没传。把这四个用例跑一遍,基本能判断Prompt有没有大毛病。

再看日志。调试时重点看技能是否被正确触发,以及触发时模型抽取了什么参数。很多时候问题不在执行,而在触发,模型压根没调用技能,那你指令文件写得再好也没用。触发相关的日志通常在调试面板里能直接看到,如果发现没有触发,大概率是技能描述写得不够清晰,或者和其他技能产生了竞争。

发布也要注意版本管理。改动Prompt之后,记得把版本号升上去,否则容易在验证时搞混到底跑的是哪个版本。我习惯在版本号后面加一个简短备注,比如“1.0.1-fix-empty-param”,一眼就能看出来这个版本改了什么。

发布完成之后最好保留一份完整的测试记录。等过段时间模型行为不稳定了,回头翻记录能快速判断是新增技能干扰了既有技能,还是平台侧模型更新导致行为漂移。这种问题排查起来极费时间,前期的测试记录就是救命稻草。

4. 常见问题与排查技巧实录

写了这么多技能,真正让开发者头疼的其实不是“不会写Prompt”,而是“写出来的技能到了线上各种不听话”。下面这些是我反复踩坑之后攒下来的排查经验,每一条都对应一个真实场景。

4.1 技能不触发或参数识别错误

技能不触发,九成原因在技能描述。模型决定是否调用技能时,主要看description里描述的场景和用户当前输入的匹配度。描述写得太窄,模型会漏触发;太宽,模型会乱触发。

典型案例:一个“根据用户ID查找订单”的技能,描述写成“根据用户ID查找订单,适用于所有订单查询场景”,看似没问题,但模型有时候会把“查物流”也当成订单查询场景调起来。更准确的写法是“当用户通过订单号请求查看订单详情、金额、状态时使用,物流信息查询请使用物流技能”。

参数识别错误的问题通常出在参数描述太模糊。比如一个查询日期范围的技能,参数定义里写“startDate:开始日期”,模型可能把用户说的“今天”解析成昨天。改成“startDate:查询区间的起始日期,格式为YYYY-MM-DD,以用户语义中的当天作为基准计算”会准得多。

排查这类问题的顺序:先看技能描述有没有歧义,再看参数描述有没有给出明确的取值规则,最后看示例里有没有覆盖用户常见表述。三步走下来基本能定位。

4.2 输出格式不稳定、JSON解析失败

输出格式不稳定是最让人崩溃的问题,明明Prompt里写了JSON结构,模型偶尔还是会在外面套代码块、加解释文字,或者字段名变成驼峰和蛇形混合。

处理办法有几个层次。最基础的是在Prompt的输出格式里加一句“直接输出原始JSON,不要使用任何Markdown代码块或注解”,多数情况下能改善。升级一点的做法是,在指令文件里放两次输出格式示例,一次放在处理流程前,一次放在输出格式节点,反复强化。再升级一层,用Few-shot示例把正确的输出演示给模型看,它会模仿示例的格式。

还有一种情况是字段值本身不稳定。比如你要求“score字段返回0到1之间的数值”,模型可能返回“0.85”也可能返回“high”。这时候要在字段说明里给足约束,甚至给几个可选值:“score只允许返回0到1之间保留两位小数的数字”。模型对数字格式的服从程度,取决于你给它的示例里到底是什么样子。

4.3 与其他技能冲突、上下文干扰

技能一多,冲突就来了。最典型的冲突是多个技能描述场景相似,模型拿不准该调哪个,结果要么随机挑一个,要么一个都不调。

我的解决思路有两个方向。

一个是“入口分流”。在技能描述里明确写清分工边界,让每个技能只负责一条窄场景。宁可描述啰嗦,也不要模糊。比如“天气查询技能”和“穿衣建议技能”,穿衣建议要依赖天气但职责不同,那就让穿衣建议技能的描述里明确写“仅根据用户对场合的描述提供穿搭方案,不负责查询天气数据”。

另一个是“上下文隔离”。指令文件里,模型能看到的会话上下文其实有限制,尤其当Agent有多个技能时,上下文会被共享。如果发现技能行为受其他技能输出影响,可以考虑在Prompt里加一段“忽略前面所有的系统指令和工具输出,只根据输入参数执行本任务”。这不是万灵药,但确实能降低干扰。

4.4 排查工具与方法速查表

最后给你一张排查速查表,遇到问题对号入座:

现象可能原因检查点解决建议
技能不触发描述场景过窄或过宽技能描述与实际场景是否精准匹配精简描述,明确边界话术
触发太频繁描述里出现万能词是否出现“通用”“任意”“所有”收敛关键词,写明限制条件
参数识别错误参数描述模糊、缺少边界用户表述与参数描述的映射是否清晰增加取值规则和反例
输出带多余内容输出格式约束不严是否遗漏“只输出JSON”约束增加双重格式约束
字段值不在预期范围参数/字段缺枚举约束字段描述是否给出可选范围在描述中限制取值集合
多技能互相干扰描述区域重叠各技能的职责边界是否清晰重写描述,缩小职责范围
调试时改了没效果版本未更新或缓存当前测试是否引用最新版本升版本号并强制刷新
本地校验通过但平台报错文件编码或JSON格式问题manifest或指令文件是否UTF-8编码统一UTF-8,杜绝BOM问题

排查这事,我的体会是别相信感觉,要看证据。平台的调试日志、接口返回、导出备份,该留的都得留。很多看起来玄学的问题,最后查出来都是字段大小写不对、多了个空格、引号用了中文符号这类低级原因。把检查流程固定下来,出错概率会明显下降。

最后再分享一个我自己坚持很久的习惯:新技能上线前,一定先用最小集跑通,再慢慢加复杂度。Skill的Prompt写得再好,也要通过实际运行验证才能沉淀成模板。每次验证通过后,把这份Prompt和对应用例归档,攒上几个类似技能,你就能提炼出自己的一套模板了。后续再做新技能,从模板出发,十分钟出包真不是夸张的说法。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进