第一节：Prompt定义及基本框架一、 什么是Prompt大模型 prompt 指在大语言模型应用中，用于 “提示” 模型唤起特定能力以解决实际问题的提问方式。早期它被称作 “输入形式” 或 “输入模板”，后来 “prompt”（提示）这一叫法因契合大语言模型语境，能准确体现其在调用模型能力方面的关键作用，成为公认术语。大模型多数核心能力在预训练阶段形成，prompt 就像钥匙，引导模型从预训练积累的海量信息中，精准唤起如理解复杂文本、总结信息、生成内容、逻辑推理等能力，满足用户实际需求。它并非把模型当作单纯知识库，而是高效调用其能力，实现类似人类运用智能解决复杂问题的效果，是 prompt 工程的核心，对用好大语言模型意义重大。二、 撰写Prompt的基本框架1、Prompt包含的要素● 指令：想要模型执行的特定任务或指令。● 上下文：包含外部信息或额外的上下文信息，引导语言模型更好地响应。● 输入数据：用户输入的内容或问题。● 输出指示：指定输出的类型或格式。2、Prompt五大框架2.1 RTF框架RTF(Role-Task-Format)框架是一个非常简单通用的Prompt提示框架，我们和任意大模型对话场景下都可以使用该规范进行改进输出。● R-Role(角色)：指定大模型担当固定角色(程序员、数据分析师、讲解员、记者等等)● T-Task(任务): 任务，告诉大模型需要为我们做的事情● F-Format(格式)：大模型最终结果的返回格式(比如：表格、Markdown、英文等等)主要优点：● 简单、方便。● 指定Role角色，可以让大模型在当前的角色范围内回答知识，这在一些特定的领域中非常有效。● 指定Role角色也能让工程上检索知识能够确定边界范围，配合元数据所发挥的威力会更强。● 如果结合RAG知识内容检索，那么上下文回答的内容会让用户感觉更加是顺畅。2.2 思考链模式通过这种模式来逐步改善大模型的推理能力，非常适合一些复杂的任务处理。例如：● 分析型或者逻辑推理型的任务● 决策● 解决问题(比如程序员根据错误日志找Bug)而要使用这种模式，只需要在末尾添加”让我们逐步思考”即可。例如：# 数据源（与指令区分） user_datasource = """XXX……""" prompt1 = """分析一下在人工客服服务场景下，'''{user_datasource}'''中客户有哪些诉求。用一句话概括。""" prompt2 = """分析一下在人工客服服务场景下，'''{user_datasource}'''中客户有哪些诉求。用一句话概括。让我们逐步思考。""" # 模型输出结果 output1：在人工客服服务场景下，客户主要诉求为：微信账号存在安全风险导致无法添加好友、单点拦截、下载安装微信出现问题等，寻求客服协助解决问题。 output2：在人工客服服务场景下，客户主要诉求为：微信账号存在安全风险导致无法添加好友，以及因违规行为被限制登录，客户希望客服能够尽快处理这些问题。 # 分析结果 output1中的”单点拦截”并不是用户的诉求，而output2显然更加正确2.3 RISEN框架● R-Role:大模型扮演的角色● I-Instructions: 指示命令，和Task-任务差不多● S-Steps: 步骤● E-End Goal: 最终目标● N-Narrowing(Constraints): 缩小范围(约束条件)，和RTF框架中的Format有异曲同工之妙，一个是格式的约束，而这里的约束可以是任意方面，比如回答的内容(特定领域)、字数限制等等方面该框架主要适合：● 撰写具有特定约束的任务(例如博客文章)● 有明确指导方针的任务（例如商业计划）2.4 RODES框架● R-Role: 角色● O - Objective: 目标● D - Details: 详细的细节● E - Examples: 示例● S - Sense Check: 感官检查2.5 密度链模式密度链模式Prompt是Salesforce、麻省理工学院和哥伦比亚大学的研究人员推出的一种新提示，它非常的高效，使用递归来创建越来越好的输出的提示，与普通提示生成的 GPT-4 摘要相比，它生成的摘要更加密集且更适合人们理解。适合：● 总结● 改进您最喜欢的提示● 通过递归生成可用的长格式内容(感兴趣的同学可以自己检索一下)第二节：打造高效Prompt的两大核心原则在Prompt工程领域，掌握精妙的撰写策略是释放语言模型强大潜力的关键。本节将深入探讨prompt撰写的两大核心策略，为您揭示如何通过优化指令，引导模型生成更贴合需求、更具质量的输出。一、原则一：编写明确和具体的指令为了让模型精准理解我们的意图，编写清晰、明确且具体的指令至关重要。这不仅能避免模型产生误解，还能显著提升输出结果的相关性和准确性。策略1：使用分隔符清晰界定输入部分在构建prompt时，使用分隔符将特定文本部分与提示的其他部分清晰隔开，能有效避免提示词冲突。任何能让模型识别出单独部分的符号都可作为分隔符，常见的包括：● 章节标题：通过不同层级的标题区分不同内容模块。● 三重双引号：""" ，在代码编写中常用于包裹较长的文本字符串。● 三重单引号：''' ，功能与三重双引号类似，适用于不同的语法环境。● 三重破折号：--- ，在文本中起到明显的分隔作用。● 角括号：<> ，常被用于标记特定元素。● XML标签：，利用结构化的标签形式区分不同内容。示例1：假设我们需要对一段产品描述进行总结，可通过如下方式使用分隔符： product_description = f"""这是一款全新的智能手表，具备心率监测、睡眠追踪等健康功能，还支持多种运动模式记录，拥有高清显示屏和长达7天的续航能力。""" prompt = f"""请总结以下产品描述：'''{product_description}'''""" 示例2：如果要让模型对一段用户评论进行情感分析，示例如下： user_comment = f"""这款手机的拍照效果真的太棒了，色彩还原度高，夜景模式也很出色，唯一不足的是电池续航有点短。""" prompt = f"""分析以下用户评论的情感倾向：''' {user_comment}''' """这种方式能够确保模型明确区分指令和待处理文本，避免因指令混淆而产生错误输出。策略2：要求结构化输出为了便于后续对模型输出进行解析和处理，我们可以要求模型以特定的结构化格式输出，如HTML或JSON。这种方式能极大提高数据处理的效率，尤其是在Python编程环境中，结构化输出可直接读入字典或列表中，方便进一步的分析与操作。示例 1：要求模型以 JSON 格式输出。prompt = "请以json格式列出每个nodeType所体现的用户诉求、客服方案。每一个json-value不超过20个字" # 数据源（与指令区分） user_datasource = """XXX……""" # 模型输出 { "IVR": { "用户诉求": "微信使用问题求助", "客服方案": "引导正常使用或自助处理" }, "ASYNC": { "用户诉求": "账号封禁等相关申诉", "客服方案": "按规则处理或引导自助" }, "AI": { "用户诉求": "支付相关问题咨询", "客服方案": "要求用户详细描述问题" } }示例2：以HTML格式输出一个简单的书籍。prompt = "请以HTML格式列出三本你推荐的科幻小说及其作者。" # 模型输出 《三体》 - 刘慈欣 《基地》 - 艾萨克·阿西莫夫 《银河帝国：机器人五部曲》 - 艾萨克·阿西莫夫 策略3：要求模型检查条件是否满足在面对复杂任务时，如果在一些假设条件，而这些条件并非总是成立，那么我们需要引导模型首先对这些假设进行检查。若条件不满足，模型应明确指示并停止继续执行任务，以避免无效计算和错误结果。同时，考虑潜在的边缘情况，并为模型设定相应的处理规则，能够有效减少意外错误，确保模型输出的稳定性和可靠性。示例：假设我们要求模型计算一个数学表达式，但前提是表达式所有都为正数。 expression = "5 + 3 - (-2)" prompt = f"""首先检查表达式 '{expression}' 中的所有数字是否都为正数。如果是，请计算该表达式的值；如果不是，请输出'表达式中存在非正数，无法计算'。"""模型输出表达式中存在非正数，无法计算策略4：Few-shot prompting（少样本提示）在要求模型执，提供成功完成任务的示例，能够帮助模型更好地理解任务要求和期望输出的格式。通过这些示例，模型可以学习到任务的模式和规律，从而在处理实际输入时生成更符合预期的结果。少样本提示尤其适用于处理一些复杂或具有特定格式要求的任务，能够显著提升模型的表现。示例：要求模型按照特定格式将中文词汇翻译成英文并造句。prompt = """ 示例1： 中文词汇：苹果 翻译：apple 造句：I like to eat apples.示例2：中文词汇：学校 翻译：school 造句：I go to school every day. 中文词汇：图书馆 翻译： 造句： """ # 模型输出 翻译：library 造句：I often read books in the library.二、原则二：给予模型充足的思考时间当模型因急于得出结论而出现推理错误时，我们需要通过优化式，为模型提供更清晰的推理路径，引导其进行更深入、全面的思考。策略1：明确完成任务所需的步骤为模型详细指定完成任务所需遵循的步骤，能够帮助模型有条不推理和计算。通过这种方式，模型可以更清晰地理解任务的逻辑结构，避免因思路混乱而产生错误。例如，要求模型按照特定格式输出，能够引导其在处理过程中注重信息的组织和呈现方式。示例：要求模型分析一首古诗，格式为“诗歌主题 - 主要意象 - 表达情感”。poem = "床前明月光，疑是地上霜。举头望明月，低头思故乡。" prompt = f"""请按照“诗歌主题 - 主要意象 - 表达情感”的格式分析这首诗：{poem}""" # 模型输出 思乡之情 - 明月 - 对故乡的深切思念 ### 策略2：引导模型在得出结论前充分思考方案 明确指示模型在得出最终结论前，先进行充分的推理和分析，往往能获得更优的结果。这种方式能够促使模型深入挖掘问题的本质，综合考虑各种因素，从而避免因片面或仓促的判断而导致的错误。示例：问模型“如何提高公司的员工满意度”。prompt = "在回答如何提高公司的员工满意度之前，请先思考至少三个方面的因素，然后综合这些因素给出全面的建议。" # 模型输出 首先，从薪酬福利方面，可适当提高薪资水平、完善福利体系，如增加带薪休假天数等；其次，从职业发展角度，为员工提供更多培训机会和晋升通道；再者，从工作环境来看，营造舒适、和谐的办公氛围，加强团队建设活动。综合这些方面，提高公司员工满意度可以从优化薪酬福利、提供职业发展机会以及改善工作环境等措施入手。三、模型的局限性与应对策略尽管语言模型在大量数据的训练下展现出强大的能力，但它们并非完美无缺。其中，“幻觉”现象是模型面个重要挑战。模型“幻觉”即使模型在训练过程中接触了海量知识，但它并不能完美记住所有信息，也难以精准把握自身知识的边界。这就导致在面对晦涩主题的问题时，模型可能会编造出看似合理但实际上错误的内容，这就是所谓的“幻觉”。解决幻觉的一种策略为了有效应对幻觉问题，我们可以要求模型在回答问题前，首先从给定文本中寻找相关引用，并依据这些引用来构建答这种方式，模型的回答将基于可靠的信息来源，而非凭空臆测，从而大大提高回答的准确性和可信度。同时，这种方法还能够实现答案的可追溯性，便于对模型输出进行验证和审查。示例：给定一段关于历史事件的文本，要求模型回答相关问题。text = """1861年，美国爆发了南北战争，这场战争是美国历史上的重要转折点，它主要围绕奴隶制的存废问题展开，北方主张废除奴隶制，南方则坚持保留。最终北方取得了胜利，为美国的工业化发展铺平了道路。""" prompt = f"""请从上述文本中找出相关信息，回答南北战争主要围绕什么问题展开？""" # 模型输出 南北战争主要围绕奴隶制的存废问题展开，北方主张废除奴隶制，南方则坚持保留。第三节：Prompt技术剖析与应用在深入探索 prompt 技术的征程中，我们将逐一解析多种关键技术，助力您精准把握其核心要点与应用场景，切实提升应用能力。一、零样本提示（Zero-Shot Prompting）零样本提示是一种让模型在没有特定任务示例展示的情况下直接处理任务的技术。其原理在于模型经过大量数据训练和指令调整后，自身具备了一定的通用知识和任务理解能力。例如：prompt = "判断此文本的情感倾向为积极、消极或中性：这部电影情节紧凑，特效惊人。情感：" # 模型会依据自身预训练知识对这个提示进行处理并输出结果模型能依据自身预训练知识输出“积极”。然而，由于缺乏具体示例引导，对于复杂任务，其效果可能受限。二、少样本提示（Few-Shot Prompting）少样本提示在零样本提示效果不佳时发挥作用。它通过为模型提供少量任务示例，帮助模型学习任务模式和规律。例如：prompt = """“whatpu”是坦桑尼亚的一种小型毛茸茸的动物。一个使用 whatpu 这个词的句子的例子是：我们在非洲旅行时看到了这些非常可爱的whatpus。“farduddle”是指快速跳上跳下。一个使用 farduddle 这个词的句子的例子是：""" # 模型会根据前面的示例学习并生成新的句子通过提供如上述新词汇造句的示例，模型能够模仿示例的结构和逻辑生成新的内容。不过在复杂推理任务中，仍需进一步优化。三、思维链提示（Chain-of-Thought Prompting）思维链提示旨在为模型提供清晰的推理步骤引导，从而显著提升其在复杂推理任务中的表现。它通过在提示中展示详细的推理过程，让模型学习如何逐步分析和解决问题。比如：prompt = """Roger 有 5 个网球，他又买了 2 罐网球，每罐有 3 个球，他现在共有多少球？ Roger 一开始有 5 个球。 2 罐网球，每罐 3 个，共 6 个球。 5 + 6 = 11。 答案是 11。""" # 模型按照提示中的推理步骤进行计算和回答此技术可与少样本提示结合，增强效果，尤其适用于算术、常识推理等任务，帮助模型更有条理地处理问题。四、自我一致性（Self-Consistency）自我一致性技术主要用于优化思维链提示中的推理路径选择。其核心思想是通过提供多个少样本推理示例，让模型从多样的推理结果中筛选出最一致的答案，增强模型在算术和常识推理任务中的可靠性。比如：prompt = """Q：林中有 15 棵树。林业工人今天将在林中种树。完成后，将有 21 棵树。林业工人今天种了多少棵树？ A：我们从 15 棵树开始。后来我们有 21 棵树。差异必须是他们种树的数量。因此，他们必须种了 21 - 15 = 6 棵树。答案是 6。 Q：停车场有 3 辆汽车，又来了 2 辆汽车，停车场有多少辆汽车？ A：停车场已经有 3 辆汽车。又来了 2 辆。现在有 3 + 2 = 5 辆汽车。答案是 5。 Q：当我 6 岁时，我的妹妹是我的一半年龄。现在我 70 岁了，我的妹妹多大？ A：""" # 模型会参考前面的推理示例对最后一个问题进行回答，并从多个可能的回答中选择最一致的答案五、生成知识提示（Generated Knowledge Prompting）生成知识提示主要用于解决模型在处理需要额外知识的任务时出现的局限性。它的操作方式是先让模型生成与任务相关的知识，再整合这些知识得出准确答案。比如：prompt = """输入：高尔夫球的一部分是试图获得比其他人更高的得分。 知识：高尔夫球的目标是以最少的杆数打完一组洞。一轮高尔夫球比赛通常包括 18 个洞。每个洞在标准高尔夫球场上一轮只打一次。每个杆计为一分，总杆数用于确定比赛的获胜者。 解释和答案：""" # 模型根据生成的知识对问题进行回答像这样先生成相关知识，再整合知识得出准确答案“不是，高尔夫球的目标是以最少杆数完成比赛，而非追求更高得分”，有效提升模型在常识推理任务的准确性。六、链式提示（Prompt Chaining）链式提示是将复杂任务拆解为多个子任务，通过逐个子任务生成提示并传递结果的方式来实现复杂任务的有序处理。以文档问答为例：# 第一个提示用于提取相关引文 prompt1 = """你是一个很有帮助的助手。你的任务是根据文档回答问题。第一步是从文档中提取与问题相关的引文，由####分隔。请使用<quotes></quotes>输出引文列表。如果没有找到相关引文，请回应“未找到相关引文！”。#### {{文档}} ####""" # 假设这里有一个文档变量 doc，将其代入提示中进行处理 # 处理后得到引文列表，假设为 quotes_list # 第二个提示根据引文和文档生成回答 prompt2 = """根据从文档中提取的相关引文（由<quotes></quotes>分隔）和原始文档（由####分隔），请构建对问题的回答。请确保答案准确、语气友好且有帮助。#### {{文档}} #### <quotes>{quotes_list}</quotes>""" # 模型根据第二个提示生成最终回答这种方式提高了任务处理的准确性与可靠性，使得模型能够逐步深入地处理复杂问题。七、思维树（ToT）思维树框架是为了帮助模型应对复杂的探索性任务而设计的。它通过维护一棵思维树，让模型在解决问题时能够生成和评估中间思维步骤，并结合搜索算法进行系统性探索。示例如下：# 假设给定数字为 4、9、10、13 prompt = """输入: 4 9 10 13 4 + 9 = 13 (left 10 13 13) 10 - 4 = 6 (left 6 9 13) 13 - 6 = 7 13 - 9 = 4 4 + 6 = 10 (left 7 9 4 * 6 = 24 (left 4 6) 评估是否能得到 24: sure """ # 模型按照思维树的步骤进行推理和计算，并根据评估结果继续搜索或得出答案八、检索增强生成（RAG）检索增强生成（RAG）技术将信息检索与文本生成相结合，专门用于处理知识密集型任务。它通过检索相关文档来为模型提供额外的知识支持，从而缓解模型的“幻觉”问题。例如：# 假设这里有一个检索函数 retrieve_documents 和一个生成答案的函数 generate_answer question = "谁是首次登上月球的人？" documents = retrieve_documents(question) prompt = "根据以下文档回答问题：" + str(documents) + " 问题是：" + question answer = generate_answer(prompt) # 最终得到的 answer 即为模型结合检索结果生成的答案这有效确保答案基于可靠知识源，在自然问题、事实验证等基准测试中表现卓越。九、自动推理并使用工具（ART）自动推理并使用工具（ART）技术使模型能够自动生成包含推理步骤的程序，并在需要时调用外部工具。例如：prompt = """计算 16 个苹果平均分给 4 个人，每人几个，再加上 3 是多少？ 请生成解决这个问题的程序步骤。""" # 模型会生成类似下面的程序步骤 # result = (16 / 4) + 3 # 然后可以在代码中执行这个程序步骤得到最终答案在 BigBench 和 MMLU 基准测试中，ART 在多种任务上表现突出，显著超越少样本提示和自动 CoT 等方法，提升了模型解决问题的能力和效率。十、自动提示工程师（APE）自动提示工程师（APE）技术能够自动生成和筛选任务指令。它通过利用大型语言模型生成指令候选项，再依据评估分数选择最佳指令，从而提升提示生成的效率与效果。比如：# 假设这里有一个生成指令候选项的函数 generate_candidates 和一个选择最佳指令的函数 select_best_instruction article = "这里是一篇新闻文章的内容" candidates = generate_candidates(article) prompt = select_best_instruction(candidates) + " " + article # 模型根据最终的 prompt 生成文章总结例如总结新闻文章时，它先利用大型语言模型生成如“提取文章关键人物、事件和时间”“概括文章主要内容并突出重点”等指令候选项，再依据评估分数选择最佳指令，指导模型生成高质量总结。十一、Active-PromptActive-Prompt 技术主要用于解决思维链示例有效性的问题。它通过先查询模型生成多个答案，计算不确定度后挑选最不确定的问题由人类注释示例，再用新示例推断其他问题，从而优化模型对不同任务的适应性。比如：# 假设这里有一个查询模型生成答案的函数 query_model 和一个计算不确定度的函数 calculate_uncertainty prompt = "对以下文本进行分类：这是一篇科技类文章。" answers = query_model(prompt, num_answers=5) # 生成 5 个答案 uncertainty = calculate_uncertainty(answers) if uncertainty > threshold: # 如果不确定度大于设定阈值 # 选择最不确定的问题由人类注释示例，然后更新提示并重新查询模型 new_prompt = update_prompt_with_annotated_example(prompt) final_answer = query_model(new_prompt) else: final_answer = select_most_consistent_answer(answers) # 最终得到的 final_answer 即为经过 Active-Prompt 处理后的答案通过这种方式，能够提高模型在文本分类等任务中的准确性。十二、方向性刺激提示（Directional Stimulus Prompting）方向性刺激提示通过训练策略 LM 生成引导提示，增强对模型生成结果的掌控力。例如文本摘要任务：article = "这里是一篇文章的内容" prompt = "请根据文章内容生成一个引导提示，重点关注文章的主要事件和结果。" # 策略 LM 会根据这个提示生成如“重点提取文章中事件的主体、发生的时间和最终结果”的引导提示 # 然后可以将这个引导提示用于后续的文本摘要任务中在文本摘要任务中，策略 LM 依据文章主题、关键信息等生成引导提示，指导模型生成符合需求的摘要，提升摘要质量。十三、PAL（程序辅助语言模型）PAL（程序辅助语言模型）技术让模型生成程序来解决问题，借助编程运行时提升解决复杂问题的能力。以日期计算问题为例：question = "今天是 2023 年 2 月 27 日，我出生于 25 年前，我的出生日期是多少？" prompt = """ import datetime from dateutil.relativedelta import relativedelta # Q: 2015 年在 36 小时后到来。从今天起一周后的日期是多少（格式为 MM/DD/YYYY）？ # 如果 2015 年在 36 小时后到来，那么今天是 36 小时前。 today = datetime(2015, 1, 1) - relativedelta(hours=36) # 从今天起一周后， one_week_from_today = today + relativedelta(weeks=1) # 格式化后的答案 one_week_from_today.strftime('%m/%d/%Y') # Q: 今天是 2023 年 2 月 27 日，我出生于 25 年前，我的出生日期是多少？ """ # 模型会生成类似下面的代码 # today = datetime(2023, 2 27) # born = today - relativedelta(years=25) # born.strftime('%m/%d/%Y') # 然后可以在代码中执行模型生成的代码得到最终答案十四、ReAct 框架ReAct 框架使模型交错生成推理轨迹和操作，提升答案的可靠性与可解释性。以回答问题为例：question = "除苹果遥控器外，哪些设备可控制其设计交互的程序？" prompt = """问题：除苹果遥控器外，哪些设备可控制其设计交互的程序？ 思考 1：我需要搜索苹果遥控器相关程序及可替代控制设备。 操作 1：搜索 [苹果遥控器 可替代控制设备] 观察 1：搜索结果为…… 思考 2：根据搜索结果，分析哪些设备符合要求。 操作 2：筛选 [符合要求的设备] 观察 2：筛选结果为…… 思考 3：整理最终答案。 操作 3：整理 [最终答案] """ # 模型会按照提示中的推理轨迹和操作步骤进行处理，并最终得出答案在知识密集型和决策任务中表现良好，通过与外部环境交互获取信息来辅助推理。十五、自我反思（Reflexion）自我反思框架包含参与者、评估者和自我反思三个模型，旨在帮助模型从错误中学习并提升性能。模拟编程任务的示例如下：# 假设这里有参与者模型 actor、评估者模型 evaluator 和自我反思模型 reflexion code = "这里是一段初始代码" # 参与者模型生成尝试解决问题的代码和动作 result = actor(code) # 评估者模型评价输出 score = evaluator(result) # 自我反思模型根据评价结果生成改进建议 reflection = reflexion(score, result) # 将改进建议应用到下一次的代码生成中，实现自我学习和提升如代码出现语法错误，自我反思模型会提示可能错误原因及修正方向，帮助模型快速学习。附录一、术语表Prompt：在大语言模型应用中，用于“提示”模型唤起特定能力以解决实际问题的提问方式，是prompt工程的核心。RTF框架：Role - Task - Format框架，通过指定角色、任务和格式规范大模型输出，简单通用。思考链模式：在提示末尾添加“让我们逐步思考”，引导大模型逐步分析复杂任务，改善推理能力。RISEN框架：包含角色、指示命令、步骤、最终目标和缩小范围（约束条件），适合特定约束或明确指导方针的任务。RODES框架：涵盖角色、目标、详细细节、示例和感官检查的提示框架。密度链模式：利用递归创建更好输出的提示，生成的摘要更密集且适合理解，适用于总结等任务。少样本提示（Few - shot prompting）：为模型提供少量任务示例，帮助其学习任务模式和规律，提升在复杂或特定格式任务中的表现。零样本提示（Zero - Shot Prompting）：模型在无特定任务示例展示下，依靠自身预训练知识和任务理解能力直接处理任务。思维链提示（Chain - of - Thought Prompting）：为模型展示详细推理过程，提升其在复杂推理任务中的表现，可与少样本提示结合。自我一致性（Self - Consistency）：通过提供多个少样本推理示例，让模型筛选最一致答案，增强在算术和常识推理任务中的可靠性。生成知识提示（Generated Knowledge Prompting）：先让模型生成与任务相关知识，再整合知识得出准确答案，解决模型处理需额外知识任务的局限性。链式提示（Prompt Chaining）：将复杂任务拆分为多个子任务，逐个子任务生成提示并传递结果，提高任务处理准确性与可靠性。思维树（ToT）：通过维护思维树，让模型生成和评估中间思维步骤，结合搜索算法应对复杂探索性任务。检索增强生成（RAG）：将信息检索与文本生成结合，检索相关文档为模型提供知识支持，缓解“幻觉”问题。自动推理并使用工具（ART）：使模型自动生成包含推理步骤的程序，并在需要时调用外部工具，提升解决问题能力和效率。自动提示工程师（APE）：利用大型语言模型生成指令候选项，依据评估分数选择最佳指令，提升提示生成效率与效果。Active - Prompt：先查询模型生成多个答案，计算不确定度后挑选最不确定问题由人类注释示例，优化模型对不同任务的适应性。方向性刺激提示（Directional Stimulus Prompting）：训练策略LM生成引导提示，增强对模型生成结果的掌控力，提升文本摘要等任务的质量。PAL（程序辅助语言模型）：让模型生成程序来解决问题，借助编程运行时提升解决复杂问题的能力。ReAct框架：使模型交错生成推理轨迹和操作，提升答案的可靠性与可解释性，在知识密集型和决策任务中表现良好。自我反思（Reflexion）：包含参与者、评估者和自我反思三个模型，帮助模型从错误中学习并提升性能。二、常见问题解答1.如何选择合适的Prompt框架？根据任务类型和需求选择。如简单对话场景可选用RTF框架；复杂分析任务可考虑思考链模式或RISEN框架；对输出格式有严格要求的，可在RTF、RISEN框架中关注格式或约束条件设定；进行总结类任务，密度链模式可能更合适。2.为什么模型会出现“幻觉”现象？模型虽经大量数据训练，但无法完美记住所有知识，也难以精准把握知识边界，面对复杂或生疏主题问题时，就可能编造看似合理但错误的内容。3.如何利用少样本提示提升模型性能？提供与任务相关、格式规范的成功示例，示例要涵盖任务的关键要素和模式，让模型学习示例中的规律和逻辑，从而在处理实际输入时生成更符合预期的结果。尤其适用于复杂任务或对输出格式有特定要求的情况。4.链式提示在实际应用中有哪些优势？将复杂任务拆解为子任务，使模型处理过程更清晰、有序，逐步深入解决问题，提高了任务处理的准确性与可靠性，避免模型在处理复杂问题时因任务过于复杂而出现混乱或错误。
20分钟聊聊clang-tidy那些事在1月初凯新有过在各算法/公共模块引入 Clang-tidy 的提议 ，在3月初我向大家分享了一个关于完善算法/业务项目CI流程的静态代码检查的文档，其中都提到了一个工具，那就是clang-tidy，今天我便带大家聊一聊clang-tidy。Clang-tidy大家应该多少对clang-tidy这个工具有一定的了解，它是一个基于clang的静态代码分析框架。同时它是一个功能非常强大的lint工具。绝大部分lint工具只能在出现问题的地方给出提示，之后需要人为修改，而clang-tidy拥有自动修复功能（当然怎么修复需要相应 checker 作者提供）。并且clang-tidy采用模块化设计，非常容易扩展。如果用户想往clang-tidy添加一个新的检测功能，只需要编写一个clang-tidy checker实现（如何编写一个clang-tidy check以后将会有一章详细介绍），每一个checker检测一种问题，例如检测某个违反Code style的模式，检测某些API不正确使用的方法等等。那么提到了Code Style，我们不得说一说Google Code Style，这是目前使用最为广泛的一种代码规范。为此Google Code style也提供了一个cpplint脚本，用于检测代码中违反code style的地方。同时clang-tidy也支持检测代码中违反google code style的地方（google-*check）。那么可能会有人问他们之间有什么区别呢？cpplint是一个python的脚本，它是采用正则表达式匹配出违反code style的代码, 所以它能检测的功能会受限于正则表达式，它不能够检测所有的违反code style的地方，并且还会有False positive和True negative；而clang-tidy则是基于抽象语法树(AST)对源文件进行分析，相比之下，是在分析的结果更加准确，能检测的问题也更多。OK 到这里我们应该对clang-tidy 进行静态代码分析的原理有了一个大致的框架它基于clang，基于抽象语法树AST实现的。那clang和AST是什么？Clang众所周知clang是一个编译器，是一个以LLVM为后端的编译前端那么clang和llvm还有什么其他关系呢？这里我们可能需要说一下广义的llvm和狭义上的llvm了。在LLVM整体架构中，前端用的是clang，因此广义的LLVM是指整个LLVM架构，而狭义的LLVM指的是LLVM后端（包含代码优化和目标代码生成）。在LLVM架构中，源代码（c/c++）经过clang生成中间代码，之后中间代码通过优化器进行一系列的优化，最后通过后端生成机器码。而在经过前端生成中间代码过程中，前端会对代码进行词法分析、语法分析以及语义分析。而在语法分析过程中变为生成一棵语法树，也就是我们说的AST。int add(int a, int b){ typedef long long ll; return a + b; }// 我们可以使用-fsyntax-only -Xclang -ast-dump 打印出源码文件的ast // clang -fmodules -fsyntax-only -Xclang -ast-dump main.m `-FunctionDecl 0x1398df030 <test.cpp:1:1, line:4:1> line:1:5 add 'int (int, int)' |-ParmVarDecl 0x1398deed8 <col:9, col:13> col:13 used a 'int' |-ParmVarDecl 0x1398def58 <col:16, col:20> col:20 used b 'int' `-CompoundStmt 0x1398df250 <col:22, line:4:1> |-DeclStmt 0x1398df198 <line:2:5, col:25> | `-TypedefDecl 0x1398df140 <col:5, col:23> col:23 ll 'long long' | `-BuiltinType 0x139840750 'long long' `-ReturnStmt 0x1398df240 <line:3:5, col:16> `-BinaryOperator 0x1398df220 <col:12, col:16> 'int' '+' |-ImplicitCastExpr 0x1398df1f0 <col:12> 'int' <LValueToRValue> | `-DeclRefExpr 0x1398df1b0 <col:12> 'int' lvalue ParmVar 0x1398deed8 'a' 'int' `-ImplicitCastExpr 0x1398df208 <col:16> 'int' <LValueToRValue> `-DeclRefExpr 0x1398df1d0 <col:16> 'int' lvalue ParmVar 0x1398def58 'b' 'int'AST从上可以看出，每一行包括AST node的类型，行号、列号以及类型的信息。在AST中有上千种node类型，我们这边简单看下常见的四种node：Type，表示一个typedefDecl，它表示程序中的一个声明，在其下面存在很多派生类。比如常见的FunctionDeclStmt，这个类表示程序中的一个语句，比如我们的while语句WhileStmtExpr，它表示程序中的一个表达式。那么既然有了这样一棵树，我们就可以通过对其节点结构做文章，精确定位到声明语句、赋值语句、运算语句等等，实现对代码的静态检查。Clang-tidy Checker而clang-tidy 的一系列checker便是去做了这件事。它在干这活的时候主要分为两部分，静态分析引擎（GRExprEngine.cpp以及其它）和 多个静态检查器（*Checker.cpp），然后所有的检查器都是建立在静态分析引擎的基础之上。那他们怎么工作的呢？首先分析器是一个源码的模拟器，追踪代码可能的执行路径。程序状态（变量和表达式的值）被封装为 ProgramState 。程序中的位置被叫做 ProgramPoint 。state 和 point 的组合是 ExplodedGraph 中的节点。分析器会沿着 ExplodedGraph 执行可达性分析（Reachability Analysis）。从具有 entry program point 和 initial state 的根节点开始，分析模拟每个单独表达式的转移。表达式分析会产生状态改变，使用更新后的 program point 和 state 创建新节点。当满足某些 bug 条件时（违反检测不变量，checking invariant），就认为发现了 bug 。最终，每个单独检查器（Checkers）也通过操作分析状态来工作。分析引擎通过访问者接口（visitor interface）与之沟通。比如，PreVisitCallExpr() 方法被 GRExprEngine 调用，来告诉 Checker 我们将要分析一个 CallExpr ，然后这个检查器被请求检查任意前置条件，这些条件可能不会被满足。检查器不会做除此之外的任何事情：生成一个新的 ProgramState 和包含更新后的检查器状态的 ExplodedNode 。如果它发现了一个 bug ，它会把错误告诉 BugReporter 对象，提供引发该问题的路径上的最后一个 ExplodedNode 节点。静态分析引擎按下图所示流程运行。所有 Checker 都由 CheckerManager 管理调度，而CheckerManager 由分析引擎内核管理。Checker 内部沿 ExplodedGraph 构造节点，遇到违反约束条件的状态时汇报错误。Checker DIYOK 到这里我们已经大概了解了checker是怎么工作的了，那我们应该怎么去编写一个自定义的checker呢？为了在ParseAST阶段快速找到自己想处理的AST 节点，clang提供了两种方式：RecursiveASTVisitor和ASTMatcher，我们今天只讲下使用clang提供的用来匹配和遍历AST 的DSL --- ASTMatcher来自定义一个checker我们在这里简单实现一个检查if条件中为非零的整型常量且body为空的checker我们先构建一个demo源文件，这里为clang-demo.cppint deeprouteDemo(int a, int b) { if (1) {} if (0.1) {} if ('a') {} if (!1) {} if (true) {} }使用前面说的clang -fmodules -fsyntax-only -Xclang -ast-dump命令查看demo的AST`-FunctionDecl 0x55984380ad38 <demo.cpp:1:1, line:7:1> line:1:5 deeprouteDemo 'int (int, int)' |-ParmVarDecl 0x55984380abb8 <col:19, col:23> col:23 a 'int' |-ParmVarDecl 0x55984380ac30 <col:26, col:30> col:30 b 'int' `-CompoundStmt 0x55984380b490 <col:33, line:7:1> |-IfStmt 0x55984380ae88 <line:2:2, col:10> | |-<<<NULL>>> | |-<<<NULL>>> | |-ImplicitCastExpr 0x55984380ae60 <col:6> 'bool' <IntegralToBoolean> | | `-IntegerLiteral 0x55984380ae40 <col:6> 'int' 1 | |-CompoundStmt 0x55984380ae78 <col:9, col:10> | `-<<<NULL>>> |-IfStmt 0x55984380b2e0 <line:3:2, col:12> | |-<<<NULL>>> | |-<<<NULL>>> | |-ImplicitCastExpr 0x55984380aee0 <col:6> 'bool' <FloatingToBoolean> | | `-FloatingLiteral 0x55984380aec0 <col:6> 'double' 1.000000e-01 | |-CompoundStmt 0x55984380b2d0 <col:11, col:12> | `-<<<NULL>>> |-IfStmt 0x55984380b358 <line:4:2, col:12> | |-<<<NULL>>> | |-<<<NULL>>> | |-ImplicitCastExpr 0x55984380b330 <col:6> 'bool' <IntegralToBoolean> | | `-CharacterLiteral 0x55984380b318 <col:6> 'char' 97 | |-CompoundStmt 0x55984380b348 <col:11, col:12> | `-<<<NULL>>> |-IfStmt 0x55984380b3f8 <line:5:2, col:11> | |-<<<NULL>>> | |-<<<NULL>>> | |-UnaryOperator 0x55984380b3c8 <col:6, col:7> 'bool' prefix '!' | | `-ImplicitCastExpr 0x55984380b3b0 <col:7> 'bool' <IntegralToBoolean> | | `-IntegerLiteral 0x55984380b390 <col:7> 'int' 1 | |-CompoundStmt 0x55984380b3e8 <col:10, col:11> | `-<<<NULL>>> `-IfStmt 0x55984380b458 <line:6:2, col:13> |-<<<NULL>>> |-<<<NULL>>> |-CXXBoolLiteralExpr 0x55984380b430 <col:6> 'bool' true |-CompoundStmt 0x55984380b448 <col:12, col:13> `-<<<NULL>>>clang-tidy 在LLVM/clang-tools-extra/clang-tidy下提供了一个名为add_new_check 的脚本文件（用 Python 编写），用以快速生成新的 checker，我们可以借助他来 init./add_new_check.py misc checker-nameinit之后它会替我们创建并更新一系列文件，短期我们只需要关心./misc/check-name.h和./misc/check-name.cpp便可以，他们是实现我们自定义checker检查的主要文件。而我们需要做的是在./misc/check-name.cpp中实现两个虚函数registerMatchers和check补充registerMatcher函数，编写匹配我们自定义规则的DSL，编写时可以使用clang-query进行验证DSL是否正确void DeeprouteChecker1Check::registerMatchers(MatchFinder *Finder) { // FIXME: Add matchers. Finder->addMatcher(ifStmt(isExpansionInMainFile(), hasCondition( implicitCastExpr( hasDescendant( integerLiteral( unless(equals(false)))))), hasThen( compoundStmt( statementCountIs(0))) ).bind("deeproute-checker-1"), this); }验证Matcher正确性$ clang-query clang-demo.cpp $ match ifStmt(isExpansionInMainFile(),hasCondition(implicitCastExpr(hasDescendant(integerLiteral(unless(equals(false)))))), hasThen(compoundStmt(statementCountIs(0)))).bind("deeproute-checker-1") Match #1: /tmp/demo.cpp:2:2: note: "deeproute-checker-1" binds here if (1) {} ^~~~~~~~~ /tmp/demo.cpp:2:2: note: "root" binds here if (1) {} ^~~~~~~~~ 1 match.补充check函数我们根据注册的matcher所绑定的节点名称进行访问，判断是否出现我们符合我们匹配要求的节点，并进行相应的警告打印void DeeprouteChecker1Check::check(const MatchFinder::MatchResult &Result) { // FIXME: Add callback implementation. if (const auto *MatchedDecl = Result.Nodes.getNodeAs<IfStmt>("deeproute-checker-1")) { diag(MatchedDecl->getIfLoc(), "The condition is an integer constant and the body is empty") << FixItHint::CreateInsertion(MatchedDecl->getLParenLoc(), "true "); } }编译clang-tidycd llvm-project mkdir build cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \ -DLLVM_ENABLE_ASSERTIONS=On \ -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" \ -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON \ -DLLVM_PARALLEL_LINK_JOBS=2 -DLLVM_BUILD_EXAMPLES=ON \ -DLLVM_INCLUDE_EXAMPLES=ON ../llvm ninja clang-tidy -j1参考文档clang-tidy-check.md · jahentao/学习LLVM - Gitee.comhttps://zhuanlan.zhihu.com/p/369254889https://code.woboq.org/llvm/clang/AST Matcher Reference
Clang 新手教程长话短说这个新手教程会让你弄清楚什么是clang，clang AST，clang plugins 和clang tools 等等。让你大概知道clang 可以解决什么问题，而且小白也是可以用clang libraries 来开发工具的 :)Clang 是什么？Clang 是一个以LLVM为后端的编译前端。编译前端主要负责parse 源码，检查错误，并生成Abstract Syntax Tree (AST)。Ref: Understanding the Clang AST什么时候会用到Clang？需要基于编译器的AST 对源码做精确的编辑需要引入自定义的编译错误和警告基于C/C++ 源码的代码生成(code generation)，比如生成数据结构的序列化，语言交互接（Foreign function interface）Clang AST 是什么？Clang AST 的节点是由几种没有共同基类的类来组成（建模）的Clang’s AST nodes are modeled on a class hierarchy that does not have a common ancestor.其中比较常用的四种class 是Type,Decl,DeclContext,Stmt每种节点都有专门的遍历函数来获取子节点ASTContext 里可以获取AST 的额外信息，比如源码的地址例子// test.cc int foo(int num) { int result = (num / 2); return result; } void test_f() { int i = 10; int b = foo(i); }clang -Xclang -ast-dump -fsyntax-only test.ccRef: Introduction to the Clang AST - Clang 15.0.0git documentationClang APIsClang 提供C 和C++ APIC API (libClang) 稳定但是不完整C++ API（LibTooling）完整但是不稳定LibTooling 主要用于开发standalone 工具，并且plugins 只能使用LibToolingRef: Choosing the Right Interface for Your Application - Clang 15.0.0git documentationClang plugins v.s. Clang toolsClang plugin 在编译时对AST 进行一些额外的操作。plugin是动态库（.so），在运行时由编译器加载，可以很容易集成到编译环境中。Clang tools 是由LibTooling 开发的可执行程序，比如clang-check，clang-format，clang-tidy 这些llvm project 提供的。有时候两种方式都能解决问题：# 1. As a loadable Clang plugin: clang -cc1 -load <BUILD_DIR>/lib/libLACommenter.so '\' -plugin LACPlugin test/LACInt.cpp # 2. As a standalone tool: <BUILD_DIR>/bin/ct-la-commenter test/LACInt.cpp考虑点：能不能控制用哪个编译器？clang / gcc?需不需要make 或者break buildClang 6.0 写的plugin Clang 7.0 不一定能用（API 不稳定！）CI 环境使用哪种工具用户接入成本更低？RecursiveASTVisitor v.s. ASTMatcherLibTooling 有两种framework: RecursiveASTVisitor 和ASTMatcherThe RecursiveASTVisitor provides hooks of the form bool VisitNodeType(NodeType *) for most AST nodesASTMatcher 是一套用来匹配和遍历AST 的DSL两种framework 可以一起使用来解决比较复杂的问题！https://github.com/banach-space/clang-tutor/blob/main/README.md#unusedforloopvar优先考虑用ASTMatcher，因为可以用clang-query 来验证DSL 是否正确clang-query test.cc# some useful commands to setup clang-query set output dump set bind-root false set traversal IgnoreUnlessSpelledInSource m functionDecl() m functionDecl(hasName("foo"))Ref:How to write RecursiveASTVisitor based ASTFrontendActions. - Clang 15.0.0git documentationTutorial for building tools using LibTooling and LibASTMatchers - Clang 15.0.0git documentationAST Matcher Reference如何写一个Clang 工具？想清楚目的写一些最小的测试用例源码并用clang -Xclang -ast-dump 来观察生成的AST找到想要匹配的AST 节点使用clang-query 来在测试用例上检验ASTMatcher将ASTMatcher 的DSL集成到clang LibTooling 的脚手架里怎么创建Clang 工具项目？In tree编译llvmClang - Getting Started注意：llvm 编译linking 占用内存非常高！所以要用单线程make，而且16G 内存的话，要将swap file 增加到16G 才能编译。ref: How to increase swap space?在llvm-project/clang-tools-extra下创建目录和写MakefileHow to write RecursiveASTVisitor based ASTFrontendActions. - Clang 15.0.0git documentationOut of treeclang-tutor/README.md at main · banach-space/clang-tutor有用的资源clang-tutor https://github.com/banach-space/clang-tutor/blob/main/README.md#clang-tutorhttps://static.linaro.org/connect/yvr18/presentations/yvr18-223.pdf
单元测试的必要性？一文聊聊单元测试
单元测试写的代码能一次正确执行是每个程序员的追求，但世事皆不能尽如人意，我们的代码经常会有 Bug，这就需要测试的存在。测试有黑盒和白盒之分。黑盒测试，测试时认为被测程序就像一个漆黑的盒子，虽然不明白其中的运行原理，但知道怎么输入有对应的输出。QA (Quality assurance)，也就是我们的测试部门一般负责对程序进行黑盒测试，调用接口时传确定的参数，再校验接口响应值符合某种预期。与黑盒测试对应的是白盒测试，白盒测试要求被测试人员了解被测程序的构造，从而构造测试用例校验程序各个分支逻辑。从这一方面来说，单元测试就是一种白盒测试。单元测试，又称为模块测试，是针对程序模块（软件设计的最小单位）来进行正确性检验的测试工作，一般对面向对象语言来说，这个最小单位是类或重要的类方法，它不仅可以用作功能测试，将单元测试集成到依赖集成工具之后，它们还可以在模块编译时执行，进行模块的回归测试。工作以来，我对单元测试的认知不断刷新，从拒绝到勉强接受，再到理解其中妙处，最近竟然有些理解我以前觉得不可思议的 TDD（测试驱动开发）了，我觉得有必要把其中心路历程整理一下，无论是之后回忆还是翻出来打脸，应该都很有意思。初遇单测不写单测刚开始工作时的公司是一个小型公司，项目小且业务简单，部门也没有要求，我是不写单元测试的。不接触就无法理解它，这时候我对单元测试的认识就是收益很低的”测试工具”，认为代码是自己写的，自己再编造一些数据去迎合这些代码，根本测试不出来什么，而且编造数据还要花费大量的尽力，收益和付出完全不成正比，程序的正确性靠 QA 就完全够了。可能跟我有类似想法的同事也有很多，后来我们干脆把所有的类方法都改成了静态方法，程序运行时不用再去创建服务对象了，这样，代码就变成了披着面向对象外衣的函数式程序。当然，这也进一步导致了单元测试不可能实行了，因为方法是层层调用的，想要构造出一组能正确运行的数据都非常困难，就更不用说再测试各种分支逻辑了。随波逐流后来换到了目前所在的岗位，部门强制要求每段逻辑都要有对应的单测 case，这样我才真正接触到了单元测试。刚开始写单元测试时内心是非常煎熬的，不认同还要被强制要求做，当然很不爽，更重要的是不习惯。原来功能开发完成后，自己改改参数调两遍接口测下就行，现在还要再写同样多甚至更多的代码来校验功能代码是否正确，花费的不仅是时间，还有心力。而且有些代码就没法写单测，费尽心思构造出数据，可能还没测出功能代码的问题，改单测 case 的 bug 就能让人崩溃了。这时我采用的策略就是仿着原来的单测 case 写，现在看来之前的很多单测 case 也没什么质量，又由于自己水平低，导致最终写出来的单测 case 基本没什么意义，要么是重要逻辑覆盖不到，要么是只能覆盖一些通用逻辑，就算有问题，调接口测一下也能测出来。收获也不能说完全没有，毕竟有非常明显的 bug 还是能够测出来的，而且有时候误改了之前的代码，也能够在 QA 反馈前及时解决，但总体来说，这样写单元测试是不划算的。单测的意义缘由后来 case 越写越多，在越来越熟练地满足单测覆盖的要求时，我也在不停思考这样的工作有什么意义，直到有一天被 leader review 代码，我感觉有些开悟了。被 review 代码的功能是将一个 json 字符串解析为服务里的配置模型，考虑到它只是一个解析字符串的功能，我把它定义为一个”工具类”，里面用静态方法实现，这样调用解析方法时不用注入 bean，使用类名.方法即可。我以为这个类虽然称不上完美无缺，但命名、日志、异常处理都很完善，至少算得上中规中矩，没想到被 leader 一顿狠批，因为他直接跳到这个工具类的上层类，发现这个类没有单测覆盖。为什么上层类没有写单元测试呢，不是因为上层的逻辑太过复杂，而且因为如果我想测这个上层类，就需要构造出一个能够解析为配置模型的大型字符串，还要传上一堆配置参数到这个解析工具类里，这个字符串不好构造不说，即使构造出来了，写在代码里这么一堆，也异常难看，如果之后要变更这个配置模型的结构，更是一场噩梦。思考被教育一顿后，我终于明白了，上层代码的单元测试难写，是因为这么一个工具类，工具类的静态方法无法 Mock 返回值，这就需要我构造大量的真实数据，费力也讨不了好，简而言之，是因为我的设计烂导致单测不好写。接着我观察了更多据说写不出单测的代码，反思了之前遇到的单测难写的代码，拿这些代码去对比代码设计原则，发现了这些代码的一个共同点：它们都严重违背了一种或更多设计原则，也就是说，它们都是烂代码。”不好写单测的代码都是烂代码”，我觉得我理解了单元测试的（部分）意义，单元测试不仅用来测试代码功能，还可以用来测试代码设计。从此之后，我开始更重视单元测试了，单元测试的名字不再用 “testMethodName” 这么敷衍的名字，也开始考虑设计单测的边界值，每次写单测时也在不停问自己，这个 case 写起来费劲吗，我的设计合理吗？终于，单测对我来说不再只是负担了。TDD 的思考TDD，测试驱动开发，是一种先写单元测试再根据单元测试写功能代码的开发模式。一直以来都觉得这种开发模式很不可思议，在自己都不知道类和方法怎么拆分时，怎么能写出单元测试呢，就算强行写出了，可是结果又跟 QA 写的测试 case 有什么区别呢？都是假想有这么一个黑盒能够对某种输出响应某种输出。在理解了单测的意义后，再往深处去想，TDD 是十分可行的。首先对于良好的代码设计，恰当的功能拆分来说，要测试的模块是基本确定的，这就提供了先写单元测试的可能性，而且这种开发方式也能有效避免业务代码开发完成后，写单元测试时发现设计不可理的窘境。虽然我不熟悉 TDD，也没有想真正实践 TDD 的开发模式，但 TDD 也能给我一些启发。我不会在功能开发完成前写单元测试，但我可以在进行代码设计前先考虑单测 case 的结构，或者先预定单测 case 的方法，功能开发完后再补充单测的方法体，这也是 TDD 的另一种实践方式吧。小结现在能明白为什么 leader 一直要求大家代码必须补充单测了，但盲目地写收益真的很小，思想上不认同的话，懒惰小人也会不停地跳出来阻止你写单元测试。如果你也跟我之前一样挣扎在单测的苦海，那你可以重新思考一下单测的意义，如果你是充满”灵性”的程序员，对我的看法有意义的话，欢迎评论。
Ubuntu Bash环境变量加载详情配置文件1、/etc/environment面对系统的环境，尽量减少对该文件的修改系统配置加载的顺序（相同变量去取后加载的）/etc/profile -> /etc/environment -> ...2、/etc/profile面对系统用户的环境变量，每一次登录时载入/etc/profile.d等配置文件会在/etc/profile 中使用一个for循环语句来调用这些脚本3、~/.bash_profile每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该文件仅仅执行一次!默认情况下，他设置一些环境变量，执行用户的.bashrc文件，建议使用~/.profile去代替~/.bash_profile4、~/.bash_login登入shell会被加载，建议不使用bash_login5、~/.profile在Debian中使用.profile文件代 替.bash_profile文件。.profile(由Bourne Shell和Korn Shell使用)和.login(由C Shell使用)两个文件是.bash_profile的同义词，目的是为了兼容其它Shell。在Debian中使用.profile文件代 替.bash_profile文件，提倡删除本地~/.bash_profile和~/.bash_login，去使用~/.profile 。6、~/.bashrc该文件包含专用于你的bash shell的bash信息,当登录时以及每次打开新的shell时,该文件被读取.7、/etc/bashrc 或者 /etc/bash.bashrc为每一个运行bash shell的用户执行此文件.当bash shell被打开时,该文件被读取.8、~/.bash_logout当每次退出系统(退出bash shell)时,执行该文件.bash配置文件加载顺序交互登录shell直接通过终端输入账号密码登录使用“su - UserName”切换的用户使用bash --login 登录配置加载顺序（并不代表文件执行顺序，相同变量取后者 ）：/etc/bash.bashrc--> /etc/profile--> ~/.bashrc--> ~/.profile--> ~/.bash_logout 非交互登录shell使用bash --login -c "echo 123"等命令执行shell配置加载顺序：/etc/profile--> ~/.profile 交付非登录shell使用Ctrl + Alt + T 呼出的终端使用bash打开的交互式终端配置载入顺序：/etc/bash.bashrc--> ~/.bashrc非交互非登录shell只使用原有系统配置
[scode type="blue"]Ubuntu 版本是 20.04 ，用的是树莓派R4B，记录一下自己实际安装过程的操作步骤。[/scode]安装 docker安装所需的软件apt-get updateapt-get install -y apt-transport-https gnupg-agent software-properties-common添加阿里云安装源的密钥curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -添加阿里云安装源add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"安装最新版 docker engineapt updateapt install docker-ce安装 k8s添加阿里云安装源curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add - cat <<EOF > /etc/apt/sources.list.d/kubernetes.list deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main EOF添加安装源密钥gpg --keyserver keyserver.ubuntu.com --recv-keys BA07F4FBgpg --export --armor BA07F4FB | sudo apt-key add -安装三驾马车apt update && \ apt install -y kubelet kubeadm kubectl && \ apt-mark hold kubelet kubeadm kubectl关闭 swap临时关闭swapoff -a 永久关闭，/etc/fstab 中注释掉 swapfile 那一行#/swapfile none swap sw 0 0配置 docker添加 docker 配置 /etc/docker/daemon.json{ "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m" }, "storage-driver": "overlay2", "registry-mirrors": ["docker镜像仓库国内加速"] }重启 dockerservice docker restart其他安装与配置如果用到 nas 挂载，需要安装 nfs-commonapt-get install nfs-common启用 kubectl 命令自动完成kubectl completion bash >/etc/bash_completion.d/kubectl参考资料：Ubuntu 国内安装 kubernetes
[scode type="blue"]本篇为自建本地apt源类笔记，便于日后记忆[/scode]Operation自建源操作，需要准备好想要放在源中的包，然后执行以下操作mkdir /var/debs cp [源中的包] /var/debs cd /var dpkg-scanpackages debs/ | gzip >debs/Packages.gz echo 'deb [trusted=yes] file:///var debs/' >> /etc/apt/sources.list apt updateSomething需要注意，每次往本地源中加包后，想要update，必需先执行下dpkg-scanpackages debs/ | gzip >debs/Packages.gz
卸载当前旧版本的cmake通过下面命令查看当前的cmake版本cmake --version若版本较旧，执行apt remove 卸载旧版的cmake若当前系统无cmake，可跳过这一步sudo apt remove cmake下载cmakecmake官网：https://cmake.org/download/sudo apt-get install -y build-essential libssl-dev wget https://github.com/Kitware/CMake/releases/download/v3.19.2/cmake-3.19.2.tar.gz tar -zxvf cmake-3.19.2.tar.gz配置cmakecd cmake-3.19.2/ ./bootstrap然后编译安装：make sudo make install hash -r这样就ok了，检查一下cmake的版本：cmake --version可以看到cmake版本为3.19.2
[scode type="blue"] FRP是一个专注于内网穿透的高性能的反向代理应用，支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。[/scode]前言因为公司属于良心型公司，几乎不加班。于是乎，不爱加班的我天天回家便各种[font color="#FFF0F5"]躺平[/font]。但是出于程序猿的自我危机感，还是决定回家后稍微蹦跶下吧。其实早在毕业前就想蹦跶了，但是因为写（~比~）毕（~较~）设（~懒~）所以没行动起来。至于为何会自己在家捣鼓起FRP，这就需要去问我[font color="#4169E1"]精致的广东男孩同事甲[/font]具体起因是因为我的title是云计算工程师，然后又想折腾点东西，便选择了折腾Cloud比较常用的管理云平台中多个主机上的容器化的应用的工具 -- k8s。因为部署k8s我需要linux系统，就装了台Ubuntu 20.04虚拟机；因为虚拟机所在宿主机不好用，便配置了虚拟机ssh环境和mac上的远程终端工具Termius，让我可以在mac上操作虚拟机；因为我想在公司使用mac能ssh处在家中内网环境的虚拟机，便要做VPN代理或者内网穿透；因为甲建议内网穿透或者找收费内网穿透服务，那肯定自己做内网穿透啊，于是甲给我推荐了FRP ~_~。为什么会是FRP通过在具有公网 IP 的节点上部署 frp 服务端，可以轻松地将内网服务穿透到公网，同时提供诸多专业的功能特性，这包括：客户端服务端通信支持 TCP、KCP 以及 Websocket 等多种协议。采用 TCP 连接流式复用，在单个连接间承载更多请求，节省连接建立时间。代理组间的负载均衡。端口复用，多个服务通过同一个服务端端口暴露。多个原生支持的客户端插件（静态文件查看，HTTP、SOCK5 代理等），便于独立使用 frp 客户端完成某些工作。高度扩展性的服务端插件系统，方便结合自身需求进行功能扩展。服务端和客户端 UI 页面。以上内容来自FRP官方文档简介，经过整个部署过程和使用体验，frp确实具有这些功能特性，同时最重要的特性便是部署是真的简单，甚至可以说是无脑，具体见下文。开始部署FRP根据官网教程去github release页面下载了最新版本的linux amd64 二进制文件。下载后便是解压了，然后将解压的文件夹拷入阿里云公网服务器（xxx.xxx.xxx.xxx）和内网虚拟机（192.168.3.54）内了。scp -r frp_0.37.0_linux_amd64/ hh@192.168.3.54:/home/hh/ scp -r frp_0.37.0_linux_amd64/ root@xxx.xxx.xxx.xxx:/root/然后ssh进入公网服务器内，将刚拷入的frp_0.37.0_linux_amd64的文件夹中内frpc相关文件删除，因为frpc为客服端文件，也就是我们虚拟机该跑的东西。cd /root/frp_0.37.0_linux_amd64 rm -rf frpc* cd systemd rm -rf frpc*删除公网服务器内多余的frp文件后，配置frpc.ini，设置了 frp 服务器用户接收客户端连接的端口，内容如下：[common] bind_port = 7000开始配置systemd 服务。使用systemd的原因是为了服务可以常驻在服务器内，开机后服务也会自动启动。首先建需要用到的配置和脚本命令软链接到它该在的地方，注意软链接时必须用绝对路径。cd /root/frp_0.37.0_linux_amd64 ln -s /root/frp_0.37.0_linux_amd64/frps /usr/bin/frps sudo mkdir -p /etc/frp/ ln -s /root/frp_0.37.0_linux_amd64/frps.ini /etc/frp/frps.ini ln -s /root/frp_0.37.0_linux_amd64/systemd/frps.service /lib/systemd/system/frps.service软链接都做完后，便可以载入systemd服务设置开机自启，并启动它了。sudo systemctl daemon-reload sudo systemctl enable frps.service sudo systemctl start frps.service到这里公网服务器部分的frp便部署好了。然后便是按照上面操作配置内网虚拟机的frpc。# 删除grps cd /home/hh/frp_0.37.0_linux_amd64 rm -rf frps* cd systemd rm -rf frps* # 配置frpc.ini 内容如下： [common] server_addr = xxx.xxx.xxx.xxx server_port = 7000 [ssh] type = tcp local_ip = 127.0.0.1 local_port = 22 remote_port = 6000 # 进行软链接 ln -s /home/hh/frp_0.37.0_linux_amd64/frpc /usr/bin/frpc sudo mkdir -p /etc/frp/ ln -s /home/hh/frp_0.37.0_linux_amd64/frpc.ini /etc/frp/frpc.ini ln -s /home/hh/frp_0.37.0_linux_amd64/systemd/frpc.service /lib/systemd/system/frpc.service # 载入systemd服务 sudo systemctl daemon-reload sudo systemctl enable frpc.service sudo systemctl start frpc.service 到这里FRP便部署好了，下面便可以开始测试了内网穿透测试FRP部署好后，测试还是很简单的，只需使用ssh，看是否能连接公网服务器的公网IP 6000端口，命令如下：ssh -oPort=6000 hh@xxx.xxx.xxx.xxx然后很不幸的是连接失败，猜测是内网虚拟机可能无法连接公网服务器7000端口。先进入到内网虚拟机中查看frpc.service的状态sudo systemctl status frps.service 2021/07/07 22:02:14 [W] [service.go:104] login to server failed: dial tcp xxx.xxx.xxx.xxx:7000: connect: connection timed out dial tcp xxx.xxx.xxx.xxx:7000: connect: connection timed out发现输出果然显示无法连接公网IP 7000端口，使用telnet xxx.xxx.xxx.x 7000进行验证发现确实无法连接，应该是公网服务防火墙没有开放7000端口的原因，使用以下命令进行开发，顺带把6000端口一并开放：firewall-cmd --zone=public --add-port=7000/tcp --permanent firewall-cmd --zone=public --add-port=6000/tcp --permanent firewall-cmd --reload 因为我的公网服务器是阿里云的，配置有安全组规则，所以去阿里云控制台修改了下安全组规则，对6000，7000端口进行暴露，然后再进行测试。ssh -oPort=6000 hh@xxx.xxx.xxx.xxx Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.8.0-59-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage 156 updates can be installed immediately. 0 of these updates are security updates. To see these additional updates run: apt list --upgradable Your Hardware Enablement Stack (HWE) is supported until April 2025. Last login: Thu Jul 8 00:37:26 2021 from 192.168.3.55 hh@ubuntu:~$测试完成，到此FRP部署正式完成，内网穿透完成，完结撒花✿✿ヽ(°▽°)ノ✿。后记后续遇到的问题多台内网服务器需要对外网暴露时，不允许有proxy名重复。也就是frpc.ini文件中的[ssh]，不能全部都叫[ssh]，否则会出现[ssh] start error: proxy name [ssh] is already in use错误，可以换个名字，例如[ssh_r4b]。