资讯动态

FewShotPromptTemplate 实战:示例选择与提示词工程避坑

发布时间:2026/10/26 18:53:33 来源:尧图企业网站定制
1. 把话说清楚还不够few-shot 在提示工程里到底补了什么模型不按你想要的格式输出这件事我遇到太多次了。你写了一大段指令告诉它只输出类别名不要解释它转头给你来一句根据评论内容分析这条应该属于物流类。指令本身没错模型也理解得没错问题出在理解和执行之间那道缝——自然语言的指令是模糊的而模型对模糊的容忍度远高于我们对它的期待。FewShotPromptTemplate这个组件本质上就是在补这道缝。它属于 LangChain 提示模块里偏工程化的一环做的是把你手写的几个示例规范化地塞进提示词里同时管好变量、管好长度、管好示例的挑选逻辑。如果你正在做需要稳定输出格式的任务——分类、抽取、改写、字段填充——并且已经被 zero-shot 的抖动折磨过几轮那这个东西值得花时间吃透。下面我按自己的实际使用顺序拆为什么需要它、它的参数怎么影响最终提示词、示例怎么选、怎么接进调用链、我在哪些地方翻过车。1.1 一个分类任务从翻车到收敛的全过程拿一个很普通的场景说把用户评论归到预设的几个类别里。类别是我们自己定的不是通用情感极性所以模型没有先验知识。第一版提示词长这样你是评论分类器。类别有体验、物流、包装、其他。请对下面的评论分类只输出类别名。跑一百条结果五花八门。有的输出物流类有的输出物流问题有的输出这条评论主要反映物流方面的不满还有的干脆输出英文logistics。格式不一致还只是表象更麻烦的是边界判断——包装盒压扁了但东西没坏这条模型一会儿判包装一会儿判体验因为体验这个词太宽它不知道我们把商品完好排除在体验之外。第二版我加了三个示例每个示例都是评论 类别的两行结构边界情况专门挑了一条放进去。同一条提示词、同一个模型、同样的温度参数格式一致率直接从六成出头跳到接近满值边界判断也稳定了。这个变化不是模型变聪明了是示例把我们心里的分类标准翻译成了模型能直接照抄的模式。这就是 few-shot 的真正价值它传递的不是知识是规范。指令告诉模型要做什么示例告诉模型做成什么样算合格。1.2 FewShotPromptTemplate 不只是字符串拼接器有人会问那我手工 f-string 拼几个示例进去不就完了为什么要引入一个类早期我确实这么干过拼出来的提示词也能用。但很快遇到三个问题。第一个是维护成本。示例散落在字符串里改一条示例要在长文本里找位置改完还要检查缩进和空行有没有破坏结构。第二个是 token 不可控。手工拼的时候你很难快速知道当前提示词占了多少长度示例一多就悄悄超限报错信息还停留在输入过长这种层面。第三个最隐蔽变量校验缺失。手写模板里如果{query}拼错成{querry}程序不会报错模型会一本正经地对着一个没被替换的占位符瞎猜你排查半天以为是模型能力问题。FewShotPromptTemplate把这几个问题一次性收掉了示例以结构化数据传入示例模板单独定义输入变量在构造时声明并校验格式化结果可以随时打印出来核对。它没有增加模型能力它增加的是你对自己提示词的可控性。1.3 什么时候该停手few-shot 的收益边界这里必须泼一盆冷水。不是所有任务加示例都有明显收益盲目堆示例反而会让效果变差、成本变高。我的经验判断是这样的如果任务依赖模型本身的世界知识比如解释一下什么是复利示例的帮助很有限甚至可能把回答带偏到示例涉及的具体场景里。如果任务的难点在于格式约束、标签体系、语气风格、判定边界这四类示例的收益最直接。尤其是标签体系这类我们自己定义的私有规则示例几乎是唯一高效的传递方式——你不可能把所有边界规则用文字穷举清楚。还有一个容易忽略的点示例数量存在明显的边际递减。多数任务三到五个高质量示例就能覆盖主要模式超过八个之后收益基本靠运气而 token 成本是线性上涨的。我现在的习惯是先上三个观察失败样本集中在哪类边界再针对性补一到两个很少超过六个。2. 四个构造参数决定最终提示词的形状FewShotPromptTemplate的参数不多但每一个都直接影响模型看到的那个字符串长什么样。很多人调不通不是因为逻辑复杂而是没搞清楚这几个参数拼装出来的顺序。我先把结构说清楚再逐个拆。最终生成的提示词大致是这样一个序列prefix 分隔符 示例1 分隔符 示例2 ... 分隔符 suffix。理解了这个顺序后面所有问题都好定位。2.1 example_prompt给每个示例做一副模具example_prompt是一个普通的PromptTemplate作用是定义单条示例长什么样。你的examples是一个字典列表每个字典的键必须能被这个模板消费掉。from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate examples [ {text: 这个功能用起来还挺顺手就是加载有点慢, label: 体验}, {text: 付款之后三天还没发货客服也联系不上, label: 物流}, {text: 包装盒压扁了里面的东西倒是没坏, label: 包装}, ] example_prompt PromptTemplate.from_template(评论{text}\n分类{label})这段代码里{text}和{label}就是模具上的两个凹槽每条示例往里一按出来就是两行文本。这种设计的好处是示例数据可以放在 JSON、CSV 或者数据库里代码只负责渲染改示例不用动代码。要注意example_prompt里能用的变量名必须和examples字典的键完全对应。多一个键不会报错只是被忽略少一个键会直接抛异常。我吃过一次亏示例数据里字段叫content模板里写成{text}跑的时候报KeyError错误信息指向的是格式化那一行跟字段来源隔了好几层找起来挺费劲。2.2 prefix 与 suffix指令在头问题在尾prefix是放在所有示例之前的那段话通常用来写角色设定、任务说明、输出约束。suffix是放在所有示例之后的那段通常包含真正的待处理输入。prompt FewShotPromptTemplate( examplesexamples, example_promptexample_prompt, prefix( 你是电商评论的分类器。把评论归入以下类别之一体验、物流、包装、其他。 只输出类别名不要解释不要加标点。 ), suffix评论{query}\n分类, input_variables[query], example_separator\n\n, ) print(prompt.format(query下单两天了物流信息一直没更新))这里有个关键细节input_variables只需要声明suffix里出现的变量因为examples里的变量已经被example_prompt消化掉了不会暴露到最外层。这一点新手很容易搞混把示例字段也塞进input_variables结果格式化时报缺少变量。另一个细节是suffix结尾的那个分类。这个尾巴不是装饰它是给模型的接力棒。示例里每条都是评论xxx\n分类yyy的结构最后一条示例结束后接上评论xxx\n分类模型自然会在这个位置续写一个类别名而不是从头开始组织语言。这种格式对齐的技巧比任何请不要输出多余内容的指令都管用。2.3 example_separator 与拼装顺序的实测差异example_separator默认是两个换行。这个参数看起来无关紧要实际影响不小。我做过一组对比分隔符用\n\n和用\n---\n在同一个分类任务上跑两百条。前者偶尔会把相邻两条示例粘连解读尤其是在示例比较短的时候后者因为有了显式的分隔标记模型对示例边界的识别明显更稳。当然代价是每次多花几个 token。如果你的示例内容本身可能包含换行比如多行代码、多段落文本强烈建议用带标记的分隔符比如\n###\n或者\nexample\n这种。否则模型分不清哪一行是示例 A 的结尾、哪一行是示例 B 的开头。还有一个我踩过的顺序问题prefix和suffix里如果都写了约束位置靠后的那个通常更有分量。如果你有一条绝对不能违反的规则放在suffix里、紧贴待处理输入的位置遵守率会比放在prefix开头高一些。这是位置效应不是模型理解能力的问题但它确实影响结果。2.4 先用 format() 把提示词打印出来再调模型这条是我最想强调的习惯。任何提示词改动之后先print(prompt.format(...))看一眼完整输出再去调模型。原因很实在你脑子里想象的提示词和实际拼出来的提示词差别往往超出预期。可能多了一个空行让模型误以为示例结束了可能某个变量没被替换留了个花括号在那里可能分隔符把最后一条示例和suffix粘成了一行。这些问题在代码层面全都不报错只有看到最终字符串才能发现。我一般会顺手把字符数也打出来粗略估算 token 消耗。中文场景下字符数和 token 数的比例大致在一比一到一比一点五之间浮动具体取决于模型的分词器。示例数量、示例长度、prefix长度三者相加很容易在你没注意的时候冲破上下文窗口。3. 示例不是越多越好选择器决定了模型看到什么示例固定写在代码里是最简单的用法也是大多数教程的用法。但真实项目里示例库会越攒越大——今天补一条边界case明天补一条线上badcase几个月后攒到几十条。这时候每次调用都把全部示例塞进去成本和延迟都受不了而且效果未必更好示例之间互相干扰是真实存在的。example_selector就是解决这个问题的。它的职责是给定当前输入从示例库里挑出最合适的那几条。注意examples和example_selector是互斥的同时传会直接报错。3.1 静态列表的适用场景先说清楚什么时候不需要选择器。如果你的示例总数在三到五条且任务是固定的、输入分布比较均匀那就老老实实用静态examples。引入选择器会多一次计算语义选择还要调嵌入模型换来的收益微乎其微反而增加了不确定性——每次挑出来的示例可能不同出问题时更难复现。静态列表的真正优势是可复现。同一份输入永远得到同一份提示词排查问题时变量少一个是一个。我现在的做法是项目早期一律静态等示例库确实超过八条、或者输入类型明显分成几大簇的时候再考虑上选择器。3.2 LengthBasedExampleSelector 与中文长度估算的偏差这是最轻量的一个选择器思路是按长度截断从示例列表开头往后取累计长度超过max_length就停。import re from langchain_core.example_selectors import LengthBasedExampleSelector def zh_length(text: str) - int: ascii_words len(re.findall(r[A-Za-z0-9], text)) cjk_chars len(re.findall(r[\u4e00-\u9fff], text)) return ascii_words cjk_chars selector LengthBasedExampleSelector( examplesexamples, example_promptexample_prompt, max_length120, get_text_lengthzh_length, )这里有个中文场景的大坑LengthBasedExampleSelector默认的长度函数是按空格切词计数len(text.split())。一段两百字的中文中间没有空格默认函数算出来是 1。结果就是长度限制形同虚设示例全量塞进去你还以为自己在控制成本。所以中文场景必须传自定义的get_text_length。上面这个函数把中日韩字符按字计数、把英文数字按词计数粗估够用。如果你想要更精确就换成对应模型的分词器但那会引入额外依赖一般项目里没必要。另外要注意这个选择器是从头往后取不是挑最相关的。所以示例列表的顺序很重要把你认为最重要的示例放前面保证它们一定被选中。3.3 SemanticSimilarityExampleSelector按相似度动态取例示例库大了之后最自然的想法是给当前输入找最像的示例。这个选择器就是干这个的把示例预先向量化存进向量库查询时把输入也向量化取最近邻。from langchain_core.example_selectors import SemanticSimilarityExampleSelector from langchain_core.vectorstores import InMemoryVectorStore selector SemanticSimilarityExampleSelector.from_examples( examples, embeddings, # 换成你自己的嵌入模型实例 InMemoryVectorStore, # 也可以换成外部向量库 k2, input_keys[text], # 关键只拿 text 字段做向量化 )input_keys这个参数容易被漏掉。默认行为是把示例字典的所有值拼起来做向量化也就是说label字段也会参与。在分类任务里这意味着物流这个标签词本身会影响相似度计算导致几条同标签的示例总是被一起选中多样性反而下降。指定input_keys[text]之后只按输入内容找相似结果合理得多。这个方案的实际代价是每次调用多一次嵌入请求。如果你的场景对延迟敏感比如实时对话可以考虑在应用启动时把常见输入的示例选择结果缓存起来或者干脆退回静态列表。3.4 多样性优先的 MMR 与自定义选择器纯相似度选择有个毛病它容易挑出几条几乎一样的示例。这在示例库里同类样本扎堆的时候特别明显——你要的是覆盖不同情况拿到的却是同一情况的三个变体。MaxMarginalRelevanceExampleSelector用最大边际相关性来平衡相关性和多样性先取一批候选fetch_k再从里面挑出既相关又互相不重复的k条。from langchain_core.example_selectors import MaxMarginalRelevanceExampleSelector selector MaxMarginalRelevanceExampleSelector.from_examples( examples, embeddings, InMemoryVectorStore, k3, fetch_k12, input_keys[text], )fetch_k和k的比值我一般控制在 3 到 4 之间。太小了多样性出不来太大了候选池里混进不相关的样本。如果这些现成的选择器都不合适自己写一个也不难只要实现BaseExampleSelector的两个方法from langchain_core.example_selectors import BaseExampleSelector class KeywordOverlapSelector(BaseExampleSelector): def __init__(self, examples, k3): self.examples list(examples) self.k k def add_example(self, example: dict) - None: self.examples.append(example) def select_examples(self, input_variables: dict) - list: query input_variables.get(query, ) q_tokens set(re.findall(r[\u4e00-\u9fff]|[A-Za-z0-9], query)) scored sorted( self.examples, keylambda e: -len(q_tokens set(re.findall(r[\u4e00-\u9fff]|[A-Za-z0-9], e[text]))), ) return scored[: self.k]这种基于字面重叠的选择器不需要嵌入模型零额外成本在垂直领域术语固定、表达方式集中效果往往不输语义方案。我在一个工单分类的项目里就用过类似的东西实测比调嵌入接口省事得多。4. 接进实际调用链字符串提示与对话式提示的两条路提示词拼好了接下来要接到模型上。这里有个分岔口字符串提示和对话式提示写法不一样选错了会在后面绕弯。4.1 字符串版本prompt | model | parser如果模型接口接受纯文本输入最直接的方式是把FewShotPromptTemplate直接接进链里。from langchain_core.output_parsers import StrOutputParser chain prompt | model | StrOutputParser() result chain.invoke({query: 下单两天了物流信息一直没更新}) print(result)invoke传进去的字典键必须和input_variables对上。这里只有query因为示例字段已经被模板吃掉了。StrOutputParser看着可有可无实际有用它保证你拿到的是干净的字符串而不是带元数据的消息对象。做批处理、写文件、入数据库的时候少了这层转换会很别扭。批量跑的时候用batch而不是循环invoke底层会并发处理速度快不少queries [{query: q} for q in query_list] results chain.batch(queries, config{max_concurrency: 4})max_concurrency别设太高接口侧的限流通常比你想的更早触发。4.2 对话版本FewShotChatMessagePromptTemplate 的角色映射现在主流的对话模型都是消息列表输入这时候用字符串模板也能跑但你会丢掉角色这个信息。示例里如果是用户问、助手答的形态用字符串拼出来的就是一整块文本模型要自己去分辨哪句是谁说的。FewShotChatMessagePromptTemplate专门解决这个每个示例渲染成一组消息带上明确的角色。from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate chat_examples [ {input: 这个功能挺顺手就是加载有点慢, output: 体验}, {input: 付款三天还没发货客服也联系不上, output: 物流}, ] example_prompt ChatPromptTemplate.from_messages([ (human, {input}), (ai, {output}), ]) few_shot FewShotChatMessagePromptTemplate( example_promptexample_prompt, exampleschat_examples, ) final_prompt ChatPromptTemplate.from_messages([ (system, 你是电商评论分类器。类别体验、物流、包装、其他。只输出类别名。), few_shot, (human, {input}), ]) chain final_prompt | model | StrOutputParser() chain.invoke({input: 包装盒压扁了东西没坏})结构上很直观系统消息定规则中间一串历史对话当示例最后一条人类消息是真实输入。模型看到的就是一个已经聊过几轮、每轮都规规矩矩回答类别名的对话现场它会自然地延续这个模式。注意example_separator在这个版本里是不起作用的因为消息本身就是天然分隔的。硬要传也不会报错但会被忽略排查问题时容易困惑。4.3 两种写法怎么选判断标准很简单看你的目标模型吃不吃消息结构。维度字符串版 FewShotPromptTemplate对话版 FewShotChatMessagePromptTemplate适用模型文本补全接口、部分自部署模型主流对话模型接口角色信息无靠文本格式暗示明确区分 human / ai分隔控制依赖 example_separator消息边界天然隔离多轮对话场景拼接复杂容易乱天然适配调试难度打印一个字符串即可需要遍历消息列表调试对话版的提示词我一般写个小工具函数把消息列表逐条打出来def dump_messages(prompt_value): for m in prompt_value.to_messages(): print(f[{m.type}] {m.content})final_prompt.format_messages(input测试输入)的返回值直接丢进去就行。养成这个习惯之后提示词出问题定位快很多。还有一个实践中的取舍如果你的示例是输入-输出这种一对一的映射关系两种写法效果差别不大字符串版更简单。如果示例本身包含多轮交互比如工具调用的完整过程、多轮澄清的对话那就必须用对话版字符串版表达不了那个结构。5. 我踩过的五个坑附排查路径这一节写的是真实翻车记录。我把当时的现象、定位过程和最终改法都写出来因为这类问题在文档里基本找不到全靠踩。5.1 花括号被当成变量报 KeyError 或占位符消失最典型的一次示例输出是 JSON 格式内容长这样{name: 张三, age: 30}。我把这段直接写进了example_prompt的模板字符串里结果运行报KeyError: name。原因是PromptTemplate把{name}当成了待替换的变量。凡是在模板字符串里出现的单层花括号都会被解析成变量占位符。解决方案有两条。第一条是转义写成{{name: 张三}}双花括号渲染成单花括号。第二条是把 JSON 内容放到示例数据里不要写死在模板里examples [ {text: 帮我提取姓名和年龄, output: {name: 张三, age: 30}}, ] example_prompt PromptTemplate.from_template(输入{text}\n输出{output})这样写完全没问题。因为output的值是在格式化之后才插入的插入的内容不会被二次解析。模板本身的花括号才需要转义变量值里的花括号是安全的。这个区别我当初琢磨了很久。如果模板逻辑复杂到转义都嫌烦可以切到 jinja2 模板格式PromptTemplate.from_template(..., template_formatjinja2)。jinja2 用{{ }}表示变量、{% %}表示逻辑跟 JSON 的花括号天然不冲突。代价是多一个依赖而且团队里其他人得看得懂 jinja2 语法。5.2 示例顺序带来的抄最后一条现象有一批数据模型输出总是莫名其妙地偏向某个类别。查了很久发现问题出在示例顺序上最后一条示例恰好是那个类别而输入本身比较模糊的时候模型倾向于延续紧邻的示例。这个现象不总出现但在输入信息量低、类别边界模糊的情况下相当明显。我的应对办法是第一别把最特殊的示例放最后。把最典型、最有代表性的示例放在紧贴suffix的位置让模型在接力时接到最稳的那一棒。第二做一轮顺序扰动测试。同一批测试数据把示例顺序打乱跑两遍如果结果差异超过你能接受的范围说明你的示例覆盖不够模型在靠位置猜。这时候该补的是示例的多样性不是调顺序。第三模糊输入的边界示例可以放两条一正一反明确告诉模型这种情况判 A、那种情况判 B。5.3 token 超限与成本失控线上跑了一周账单比预估高了四倍。排查发现是示例库从最初的三条涨到了十几条每次调用都全量塞进去单次请求的 token 翻了好几倍。这个问题的根源不是FewShotPromptTemplate本身是没人盯着示例库的增长。我后来的做法是在构造提示词的地方加一个预算检查把format之后的字符串长度打日志超过阈值就告警rendered prompt.format(**payload) if len(rendered) 2000: logger.warning(prompt too long: %d chars, len(rendered))然后把静态列表换成LengthBasedExampleSelector设一个明确的max_length。配合前面说的中文字数函数限制才真正生效。还有一点值得提示例长度本身应该被管理。一条示例写得像篇小作文会挤掉其他示例的位置。我现在的规范是单条示例的输入控制在 60 字以内输出控制在 20 字以内。示例是用来示范格式和边界的不是用来展示信息量的。5.4 示例格式不一致导致输出漂移这个坑最隐蔽。示例是这样的examples [ {text: ..., label: 物流}, {text: ..., label: 体验。}, # 多了个句号 {text: ..., label: 包装 }, # 多个空格 ]人眼看着没什么模型学到的却是输出带标点也行、带空格也行。上线后输出要清洗一遍才能入库清洗规则又写不全数据里混进了各种变体。排查这类问题的办法是写个校验函数在构造提示词之前把示例跑一遍VALID {体验, 物流, 包装, 其他} for ex in examples: if ex[label] not in VALID: raise ValueError(fbad label: {ex[label]!r})看着有点啰嗦但它挡住的是最难查的一类问题。示例是模型的标准答案标准答案本身不规范后面所有环节都是在给脏数据擦屁股。5.5 版本迁移时的导入路径与废弃参数LangChain 这两年的模块拆分比较频繁。老代码里from langchain.prompts import FewShotPromptTemplate还能用但新项目建议统一走langchain_core.promptslangchain_core.example_selectors也是新路径。老路径很多是转发短期没影响长期容易在某次升级后突然报导入错误。还有一个参数validate_template。这个在早期版本里用来校验模板变量是否匹配后来被标记废弃并在新版本里移除。看到老教程里传这个参数直接删掉就行传了反而可能报错。升级时我一般会先跑一个最小可运行脚本把format结果打印出来人工核对一遍再跑测试集。光看测试通过率不够因为提示词的变化有时候不影响通过率但影响输出格式下游解析会挂。6. 把示例当资产管起来版本化、评估与动态注入示例积累到一定数量后它就不再是代码里的几行常量了而是一份需要维护的资产。我现在的项目里示例库的重要性跟训练数据差不多管理方式也借鉴了一些数据管理的思路。6.1 示例库的目录组织与字段设计我一般把示例放在独立文件里按任务分文件格式用 JSON 或 YAML[ { id: cls-001, task: review_classify, text: 付款三天还没发货客服也联系不上, label: 物流, note: 覆盖物流客服两个槽位的复合描述, added_at: 2025-01-08, source: 线上badcase } ]note字段看着多余其实最有用。它记录的是这条示例为什么存在于示例库三个月后你回头看能立刻判断它还需不需要。source记录来源线上 badcase 和人工构造的示例权重不一样出问题时追责路径完全不同。代码里只做加载和校验import json with open(examples/review_classify.json, encodingutf-8) as f: raw json.load(f) examples [{text: e[text], label: e[label]} for e in raw]传给模板之前做一次字段裁剪把note、source这些元数据去掉。多传字段不会报错但会让格式化流程多一层不确定性没必要。6.2 用一个小评估集做 A/B改示例之后效果到底变好还是变差不能凭感觉。我一般维护一个五十到一百条的小评估集带人工标注的正确标签每次改完示例跑一遍。评估集不能随便抽。我的构造方式是正常样本占六成边界样本占三成故意刁难的样本占一成。正常样本保证基础能力没退化边界样本是重点观察对象刁难样本用来发现看起来没问题但一旦遇到极端输入就崩的情况。对比方式就是跑两个版本输出一个简单表格版本示例数格式合规率边界样本准确率平均字符数v1 静态三条396%71%780v2 静态五条599%84%1180v3 相似度选择 k3398%87%920这种表格能直接告诉你多加两条示例换来 13 个百分点值不值。多数时候是值的但也有时候加了示例准确率反而掉——那通常意味着新增的示例跟原有示例在边界上冲突了得先把标准统一清楚。跑评估的时候记得把温度设成 0或尽可能低。温度不为零的情况下同一份输入跑两次结果不同评估结论就不可信了。6.3 结构化输出场景里的示例设计最后说一个我觉得 few-shot 收益最大的场景结构化抽取。需求是从一段自由文本里抽出几个字段输出必须是严格 JSON。这种情况下指令写得再清楚模型也常常给你加个以下是提取结果的前缀或者用单引号或者少个字段。示例在这里的作用是给出完整的输出契约。我一般放三条示例覆盖三种情况字段全有、部分字段缺失用 null 占位、文本里完全没有相关信息输出全 null。examples [ { text: 联系人王工电话 138xxxx1234下周三上门, output: {contact: 王工, phone: 138xxxx1234, time: 下周三}, }, { text: 客户说时间还没定先留个电话 139xxxx5678, output: {contact: null, phone: 139xxxx5678, time: null}, }, { text: 这周暂时不需要安排, output: {contact: null, phone: null, time: null}, }, ]第三条最关键。没有它模型遇到没信息的输入时会硬编一个值出来或者干脆不输出。有了这条全 null的示范缺失情况的处理就有了依据。配合example_prompt PromptTemplate.from_template(文本{text}\n结果{output})再在suffix里写文本{query}\n结果输出就直接从{开始解析成功率非常高。这种示例即契约的用法比写十行请严格输出 JSON 格式的指令有效得多。后续还能往下扩把示例库按业务线拆分不同场景加载不同示例集或者把线上新发现的 badcase 定期回流进示例库形成一个闭环。不过这些都属于锦上添花先把三到五条高质量示例写扎实比什么机制都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑