一、COT-思维链的使用
CoT是一种改进的Prompt技术,目的在于提升大模型LLMs在复杂推理任务上的表现,对于复杂问题尤其是复杂的数学题大模型很难直接给出正确答案。如算术推理(arithmetic reasoning)、常识推理(commonsense reasoning)、符号推理(symbolic reasoning)。COT通过要求模型在输出最终答案之前,显式输出中间逐步的推理步骤这一方法来增强大模型的算数、常识和推理能力。简单,但有效。
简单来讲,思维链就是一系列中间的推理步骤(a series of intermediate reasoning steps),通过让大模型逐步参与将一个复杂问题分解为一步一步的子问题并依次进行求解的过程可以显著提升大模型的性能。
一个完整的包含 CoT 的 Prompt 由指令(Instruction),逻辑依据(Rationale),示例Examples三部分组成。
- 指令:用于描述问题并且告知大模型的输出格式;
- 逻辑依据:指 CoT 的中间推理过程,可以包含问题的解决方案、中间推理步骤以及与问题相关的任何外部知识;
- 示例:指以少样本的方式为大模型提供输入输出对的基本格式,每一个示例都包含:问题,推理过程与答案。
以是否包含示例为区分,可以将 CoT 分为 Zero-Shot-CoT 与 Few-Shot-CoT:
- Zero-Shot-CoT 不添加示例而仅仅在指令中添加一行经典的“Let’s think step by step”,就可以“唤醒”大模型的推理能力。
- Few-Shot-Cot 则在示例中详细描述了“解题步骤”,让模型照猫画虎得到推理能力。
CoT 大幅度提高了 LLM 在复杂推理任务上的表现,并且输出的中间步骤方便使用者了解模型的思考过程,提高了大模型推理的可解释性。目前,思维链推理已经成为大模型处理复杂任务的一个常用手段。
优点
CoT 的能力已经被无数工作所验证,总结归纳为以下四点:
- 增强推理能力:CoT通过将复杂问题分解为多步骤的子问题,显著的增强了大模型的推理能力,也最大限度的降低了大模型忽视求解问题的“关键细节”的现象,使得计算资源总是被分配于求解问题的“核心步骤”。
- 增强可解释性和可信度:中间步骤的展示使得模型的推理过程更加透明,用户可以判断模型是否正确理解问题。
- 增强可控性:通过逐步展示推理过程,用户可以在模型的推理过程中施加更多控制,避免其成为不可控的“黑盒”。
- 提升灵活性与创造性:仅仅添加一句“Let’s think step by step”,就可以在现有的各种不同的大模型中使用 CoT 方法,同时,CoT 赋予的大模型一步一步思考的能力不仅仅局限于“语言智能”,在科学应用,以及 AI Agent 的构建之中都有用武之地。谷歌通过这种方式找到了更好的一句:“Take a deep breath and work on this problem step-by-step”,让GSM8K的结果直接从 71.8% 上升到了 80.2%(待验证)。
缺点
- 设计复杂:COT需要用户提供明确的推理过程,设计起来较为繁琐。
- 不适用于所有问题:对于某些问题,尤其是需要专业知识的领域,COT可能不适用。
- 过度依赖推理过程:模型可能过于依赖给定的推理步骤,忽略其他可能的解决方案。
适用场景
从工程的角度而言,CoT 的适用场景抽象一下可以被归纳为三点:
-
- 适用于使用大模型(模型参数在20B以上)
-
- 任务需要复杂推理
-
- 参数量的增加无法使得模型性能显著提升
现有的论文实验也表明,CoT 更加适合复杂的推理任务,比如计算或编程,不太适用于简单的单项选择、序列标记等任务之中,并且 CoT 并不适用于那些参数量较小的模型(20B以下),在小模型中使用 CoT 非常有可能会造成模型幻觉等等问题。
二、格式化输入
格式化输入对于提升大模型的表现至关重要。清晰的结构能够帮助模型更好地理解任务要求,并生成符合预期的输出。以下是一些常见的格式化技巧:
以下是在设计与开发过程中总结的一些技巧:
- 合理使用空行和标识符:例如,使用#,<>“``”、[],- ``````等符号来区分各个部分,使得输入更加清晰。
- 使用Markdown语法:大模型能够解析Markdown格式,可以借助 MarkDown 语法进行 Prompt 结构的整理。
- 选择合适的格式:根据不同的大模型(如Claude3.5、GPT等),选择不同的输入格式,如XML标签或Markdown格式。
以下是在调试过程中使用的格式,主要有Markdwn格式、Jinja2以及XML格式等。
1. 角色定义标签:
<role> <name>财务分析师</name>
<expertise>财务报表分析、预算规划、风险评估</expertise>
<style>专业、严谨、数据驱动</style>
</role>
这种标签可以帮助模型更好地理解和扮演特定角色,提供更专业准确的回答。
2. 任务说明标签:
<task>
<objective>分析公司第三季度财务报告</objective>
<deliverables>
<item>收入增长率</item>
<item>现金流状况评估</item>
</deliverables> <constraints>
<time>30分钟内完成</time>
<format>简洁报告,不超过500字</format>
</constraints>
</task>
清晰定义任务目标和要求,有助于模型生成更符合预期的输出。
3. 背景信息标签:
<context>
<industry>电子商务</industry>
<company_size>中型企业,500-1000名员工</company_size>
<market_conditions>竞争激烈,增长放缓</market_conditions>
</context>
提供相关背景可以帮助模型生成更贴合实际情况的回答。
4. 数据输入标签:
<data>
<source>公司内部财务系统</source>
<time_period>2023年第三季度</time_period>
<key_metrics>
<revenue>1亿元</revenue>
<net_profit>500万元</net_profit>
<operating_expenses>8000万元</operating_expenses>
</key_metrics>
</data>
明确的数据输入可以降低模型生成虚假数据的可能性。
5. 输出格式标签:
<output_format>
<structure>
<section>执行摘要</section>
<section>详细分析</section>
<section>建议</section>
</structure>
<style>简洁专业,使用要点列表</style>
<length>不超过1000字</length>
</output_format>
指定输出格式有助于获得结构化且一致的回答。
6. 验证和引用标签:
<validation>
<required_sources>
<source>公司财务报告</source>
<source>行业分析报告</source>
</required_sources>
<citation_format>标准APA格式</citation_format>
</validation>
这类标签可以提醒模型提供可验证的信息和适当的引用。
7. 限制和警告标签:
8.
<limitations>
<scope>仅基于公开可得信息进行分析</scope>
<disclaimer>本分析不构成投资建议</disclaimer> <
/limitations>
明确限制可以帮助模型避免过度推测或做出不当承诺。
三、调试Prompt
试Prompt是优化大模型输出的一种有效方法。通过使用现有的大模型(如GPT-4、Claude3.5等)生成、优化和评分Prompt,可以逐步提升模型的准确性。调试过程中,可以不断调整Prompt的结构和内容,直到模型输出符合预期。
四、使用Jinja2简化工作流
Jinja2是一种流行的Python模板引擎,可以帮助简化大模型工作流的开发和调试。在代码编程场景中,通过Jinja2模板,可以将相似的工作流合并,减少重复代码,提高开发效率。使用Jinja2模板后,开发人员可以更加灵活地调整Prompt的内容,快速适应不同的场景需求。
例如,在代码编程场景开发初期,针对同一种功能不同场景以及不同的需求,
我们在Dify平台做了很多的分支工作流,但是在每个分支下,相同Prompt的部分占比高达20%,因此使用不同的分支来适配不同的场景,开发和调试及其耗费时间和人力成本。
好在Dify平台支持jinja2模板,我们在经过优化发布了第二版本的Prompt,使用markdown+jnja2结合,将原先的多工作流合并为了一个工作流,极大的简化了后期调试、开发以及适配(包括模型测试)等人力成本。
以下分别是代码编程(生成单元测试功能)的使用迭代过程:
v1版本(不使用jinja2格式)
v2版本(使用jinja2格式)
使用jinja2格式后,缩减了工作流,使用jinja2格式中的if代码,减少的共用模块(Role、Goals、Constraints等的重复使用)
如果不适用jinja2模式,那么针对不同语言(Python、C、Go等)需要设定三个工作流分支,以及重复利用共有的prompt部分
# Role: Programming Expert
## Goals:
Quickly understand the "{{ language }}" code snippet and its context, analyze the code and test it according to the test plan.
Write a set of unit tests based on the "{{ test_frame }}" framework to verify the function's behavior.
{% if explain %}
## Explains:
{{ explain }}
{% endif %}
## Constraints:
Please generate unit test code based on the test points, adhering to these requirements for the generated tests:
- Do not include a main function in the test code.
- Do not recreate the class or function being tested within the test code.
- Do not include code for test execution.
## Skills:
- Proficiency in generating complete unit test code, including correct import of dependency packages.
- In-depth knowledge of the {{ test_frame }} testing framework and its best practices.
- Understanding of code coverage principles and how to achieve high test coverage.
{% if test_points %}
## Test Plan:
{{ test_points }}
{% endif %}
## Workflows:
Please think through the following steps one by one:
- Analyze the code to be tested and the test plan, and strictly follow the test plan for testing.
- Construct reasonable import information based on the code to be tested, analyzing as follows:
- The function to be tested is located in the file: {{file_path}}. Please write the import information for the function to be tested based on the import information in the code and the file path.
- Correctly import the packages used for the generated test code.
- No need for additional class object creation, please reuse the class objects required by the function to be tested.
- Generate complete {{test_frame}} unit test code without execution code and without extra explanations.
{% if language == 'python' %}
- Ensure all necessary modules are imported, including pytest and mock libraries.
- Current Python version: {{ py_version }}.
- If it's Python 3, use @pytest.mark.parametrize to decorate inputs and outputs.
- For mock functions in the test plan, if it's a method, please use "patch" to create; if it's an attribute or object, please use MagicMock to create.
- If it's Python 2, please use the mock library to generate mock functions.
{% endif %}
{% if language == 'go' %}
- Use Convey and {{ mock_mode }} as the testing framework.
- Reuse the package of the original function.
{% endif %}
{% if language in ['c','c++'] %}
- Use {{ test_frame }} as the testing framework.
{% endif %}
{% if language not in ['python', 'go', 'c','c++'] %}
## Xtest Framework Introduction(for non-mainstream languages)
For {{ language }}, we use xtest as the testing framework. xtest is an internally implemented C-based testing library, with syntax similar to gtest.
It provides runtime stubbing capabilities, allowing for more flexible unit testing.
Key features:
- Syntax resembles gtest for ease of use.
- Provides runtime stubbing via xtest_replace function.
- Allows flexible unit testing through function replacement.
Refer to the provided "Xtest Sample Code" for implementation details.Demonstrating how to use this framework for testing and stubbing.
## {{ language }} Code
{{ function_and_context }}
## Output
五、合理使用大模型参数
在使用大语言模型时,除了调整输入内容,还有两个重要参数可以调节:温度(Temperature)和Top-P。这两个参数控制着模型输出的确定性和多样性。
大模型的本质是在 Token 的概率空间中进行选择,依据概率选择接下来要输出的 Token,而这 2 个参数就是在控制这个过程。
温度(Temperature)
- 温度是一个0到1之间的数值
- 低温(接近0):输出更确定,倾向选择高概率词
- 高温(接近1):输出更随机,词的选择更均匀
当温度接近 0 时,输出文本会变得更加确定,模型更倾向于选择具有较高概率的词,这可能导致生成的文本质量较高,但多样性较低。
当温度接近 1 时,输出文本的随机性增加,模型会更平衡地从概率分布中选择词汇,这可能使生成的文本具有更高的多样性,但质量可能不如较低温度时的输出。
温度大于 1 时,输出文本的随机性会进一步增加,模型更可能选择具有较低概率的词
Top-P
- Top-P 控制模型从概率最高的词中选择的范围。
- 值越小,选择范围越窄,模型更倾向于选择高概率词。
- 值越大,选择范围越广,模型可能选择更多样化的词。
举个例子,帮助大家理解。
假设有一个语言模型,它正在预测句子中的下一个单词。
我们输入的句子是我喜欢吃苹果和____,
那么模型可能会为香蕉分配 0.4 的概率,为橙子分配 0.2 的概率,为鸭梨分配 0.2 的概率,为白菜分配 0.1 的概率,为萝卜分配 0.1 的概率。
假设Top-P = 0.8,则我们会按照概率大小选择尽可能多的词,并让概率的总和小于0.8。因此我们会选择 “香蕉”,“橙子”,“鸭梨”,而如果再加上 “白菜” 则累计概率会超过阈值 0.8。
最后模型会在 “香蕉”,“橙子”,“鸭梨” 中随机选择一个单词。在这个例子中,我们有 50% 的几率会选择 “香蕉”,25% 的几率选择 “橙子”,25% 的几率选择 “鸭梨”。
温度的影响
- 温度 进一步影响了这个选择过程。假设温度为0.5,那么模型在选择词时会更加倾向于高概率词,即“香蕉”的选择概率会增加,而“橙子”和“鸭梨”的选择概率会减少。
- 如果温度为1.0,模型会更加随机地选择词,即“香蕉”、“橙子”和“鸭梨”的选择概率会更加均匀。
- 如果温度大于1,模型会更倾向于选择低概率词,即“橙子”和“鸭梨”的选择概率会增加,甚至可能选择“白菜”或“萝卜”。
总结而言,温度(Temperature)和 Top-p 是对模型输出确定性的控制
可以根据具体的应用场景进行调试:
- 需要模型确定稳定的产出结果:设置更高的确定性,提升模型应用的稳定性。
- 需要模型提供多种结果或更具想象力:设置更高的多样性,使模型生成更多样化的文本。
在代码设计过程中,针对不同的应用场景与不同的需求,可以设定不同的参数值,
在代码生成领域
例如,
- 在自动化用例测试中(生成单测),我们需要输出更确定、更倾向于高概率的代码,温度设置在0.1-0.2,top-p小于0.5
- 在生成新代码场景中,例如生成新的接口、修复bug以及新增字段,代码需要更加多样性,那么温度和top-p应设置在接近1左右。
- 如在设定在代码补全、代码辅助工具的场景中,需要质量与多样性平衡的场景。
六、写好Few-shots
什么是Few-shots
“few-shots”,指的是训练机器学习模型时只使用极少量的标注样本。它的目标是使机器学习模型在只有少量标注样本的情况下也能达到良好的性能。
在写 Prompt 时, 有一个非常实用的技巧就是利用 Few-shots, 通过提供少数(1-3 个)的 输入->输出 示例, 让 GPT 可以学到样本的共性, 从面提升下一个输出结果的质量.
怎么用Few-shots
我们可以在如何写好Prompt: 结构化的基础上, 增加一个结构块: “## Examples:”, 在该结构块举 1-3 个示例, 从而进一步提升 Prompt 带来的输出结果提升.
优势
使用 Few-shots 技巧的好处在于, 脱离文字描述你的需求, 直观地告诉 GPT 你想要的输出具体是什么样.
- 人类大脑的认知: 读取的是"意义", 经过"逻辑思考", 输出的也是"意义".
- LLM 大脑的认知: 读取的就是一个个的 Token, 输出的也是一个个的 Token(概率最高的).
Few-shots 就是根据 LLM 大脑的特性, 来喂给它习惯吃的食物.
Few-shots在COT的应用
关于上文在COT介绍中提到的Few-Shot-CoT示例,并不是简单举出几个例子就能适合生产环境,在过往的测试环境下(仅针对代码生成领域),Few-Shot-CoT的使用要简单、明确、覆盖范围广。并且要在不同场景不同语言中适配。
在代码编程场景中,例如解释代码、代码debug、增加日志等功能,每个功能的Few-Shot-CoT不低于3个并且包含Python、C/C++以及Go语言等,并且适用于特别经典的场景,每个示例的测试不低于30+次,一但不适用,及时寻找新的示例再进行测试,否则会影响大模型过度依赖示例从而降低大模型的推理过程和准确率。
如下所示:
下面的例子是在代码编程领域的一个例子(解释代码功能)
其中Few-Shot-CoT的适配经过了反复的寻找与测试,最终达成的效果准确率符合预期。
## role
You are an expert in all programming languages.
## Instructions
Your tasks can be summarized as follows:
- Conduct an in-depth analysis of the provided **<Source Code>**,identifying key logic, functionality, and design intentions.
- First, summarize the main functionality in the <Source Code>.
- Proceed to explain the key implementation steps and methods, including any special algorithms or technical applications used.
- Highlight details that may be overlooked or require special attention.
- Avoid redundancy; do not explain basic knowledge or overly obvious parts of the code;
- Avoid redundancy; focus on crucial information for understanding the code's functionality and design.
- Ensure explanations are clear and accurate for quick comprehension of the code's core points.
- **<Context Code>:** The is related code in the same file as and can be used as a reference,but its interpretation is prohibited.
- **Render functions, methods, class names, etc. in bold in markdown**.
- **Output Requirements:**
- Do not output topics unrelated to the code, only focus on explaining the code.
- Ensure responses **strictly adhere** to **<Output Example>**.
- **Respond in Chinese**.
Please refer to the <Chain-of-Thought example> and think step by step.
## Chain-of-Thought example
### Source Code
func outputFilter(reader io.ReadCloser) {
scanner := bufio.NewScanner(reader)
go func() {
defer reader.Close()
for scanner.Scan() {
content := scanner.Text()
// 每个输出都记录到文件中
logrus.Info(content)
re := regexp.MustCompile(`(?m)^.*ERROR.*$`)
// 过滤任务标题,详细信息不展示,记录到日志中
if strings.HasPrefix(content, "[") {
fmt.Println(content)
} else if strings.HasPrefix(content, "TASK") {
fmt.Println(content)
} else {
matches := re.FindAllString(content, -1)
// 输出匹配的行
for _, match := range matches {
fmt.Println(match)
}
}
}
}()
}
### Output Example
#### 主要用途:
这段代码的主要用于从输入中过滤出包含 "ERROR" 的行,并将所有行记s录到日志中。此外,它还会打印以 "[" 或 "TASK" 开头的行。
#### 实现步骤:
这段代码定义了一个名为 `outputFilter` 的函数,该函数接受一个 `io.ReadCloser` 类型的参数 reader。
以下是该代码中关键步骤的逐步解释:
1. 使用 `bufio.NewScanner` 创建一个新的扫描器来读取 reader 中的数据。
2. 启动一个新的 goroutine 来处理读取的数据。
3. 在协程中,使用 defer 关键字确保在 Goroutine 结束时,reader 被正确关闭。
4. 使用正则表达式 (?m)^.*ERROR.*$ 匹配包含 "ERROR" 的行。
5. 如果行以 [ 或 TASK 开头,则直接输出该行;否则,输出所有匹配正则表达式的行。
#### 注意事项:
- **资源泄露**:虽然使用了`defer reader.Close()`来关闭资源,但如果`scanner.Scan()`在读取过程中出现错误,协程可能会提前退出,导致资源未被释放。应确保所有路径都能正确关闭资源。
- **并发安全性**:虽然这段代码中并没有显式的共享状态修改,但在并发编程中,需要注意对共享资源的访问(如全局日志文件)可能需要同步机制来避免竞态条件。
- **错误处理**:代码中没有异常处理机制,例如`scanner.Scan()`在读取过程中可能会遇到的错误。在实际应用中,应该添加错误处理逻辑来确保程序的健壮性。
- **正则表达式性能**:正则表达式的编译是在循环内部进行的,这可能会导致不必要的性能开销。应该将正则表达式的编译移至循环外部,只编译一次。
## Chain-of-Thought example
### Source Codes
def format_condition_receiver(self, id_receiver_map):
"""填充接收对象"""
# 1.获取单个排除规则勾选的所有接收对象id
receiver_id, enable = self.get_receiver_id()
if not enable:
return
# 2.默认初始化
self.init_receiver_config(enable)
# 3.填充接收对象的address_config字段
try:
for it in receiver_id:
# 接收对象
receiver = id_receiver_map[it]
if not receiver or not receiver.status or \
receiver.type != ReceiverType.business_system:
continue
# 增加配置信息
self.condition(receiver.type, receiver.address_config)
except:
pass
### Output Example
#### 主要用途:
这段代码主要用于设置通知或消息传递系统中接收者的详细信息,例如他们的地址、联系方式或接收消息的条件。
#### 实现步骤:
这段代码定义了一个名为 `format_condition_receiver` 的方法;该方法的主要目标是配置接收对象的 address_config 字段。
以下是该代码中关键步骤的逐步解释:
1. 通过调用 `self.get_receiver_id()` 方法获取所有被选中的接收对象的ID和启用状态。
2. 如果启用状态为假(enable 不为真),则方法直接返回,不执行任何操作。
3. 如果启用状态为真,调用 `self.init_receiver_config(enable)` 方法来初始化接收配置。
4. 遍历所有接收对象的ID,并使用 `id_receiver_map` 字典来查找对应的接收对象。
5. 对于每个接收对象,检查其是否存在、状态是否为激活,并且类型是否为 ReceiverType.business_system。
6. 如果接收对象满足上述条件,则调用 `self.condition(receiver.type, receiver.address_config)` 方法来增加配置信息。
#### 注意事项:
- **错误处理**:该方法中使用了一个空的 `except` 块来捕获并忽略所有异常。这种做法通常不推荐,因为它会隐藏潜在的错误,使得调试和维护变得困难。应该至少记录异常信息或对可能的异常进行特定处理。
- **状态检查**:在增加配置信息之前,方法检查接收对象的状态和类型。这意味着只有满足特定条件的接收对象才会被处理。这是一个重要的逻辑点,因为它决定了哪些接收对象会被包含在配置中。
- **方法命名**:`self.condition` 方法的命名可能不够明确,不易理解其具体功能。在实际代码库中,应该使用更具描述性的方法名来提高代码的可读性。
- **循环中的条件判断**:在循环中对每个接收对象进行条件判断,这可能会影响性能,尤其是当 receiver_id 列表很大时。如果可能,应该在循环之外进行优化,例如通过预先筛选出满足条件的接收对象。
## Context Code
{{ code_context }}
## Source Code
{{ code }}
## Output
七、总结与思考
通过结合COT技术、格式化输入、调试Prompt、Jinja2模板和合理的参数设置,可以显著提升大模型的准确性和减少幻觉。这些方法不仅能帮助大模型更好地理解复杂任务,还能增强其可解释性和可控性,从而在工程应用中取得更好的效果。