2024年3月

pandas

DataFrame
可以通过设置参数使得在
jupyter notebook
中显示的更加美观,
但是,将
DataFrame
的数据导出
excel
时,却只能以默认最朴素的方式将数据写入
excel

本文介绍一种简单易用,让导出的
excel
更加美观的方法。

1. 概要

首先,引入一个库
StyleFrame
,这个库封装
pandas

openpyxl
,让我们轻松的设置
DataFrame
的样式并导出到
excel
中。

安装很简单:

pip install styleframe

这个库主要包含3个模块:

  1. styleframe
    :相当于这个库的主入口,它封装了
    DataFrame
    对象。
  2. styler
    :用来单元格的样式。
  3. utils
    :常用样式元素的辅助类,比如数字和日期格式、颜色和边框类型等。

安装成功之后,下面通过示例看看如何使用。

2. 准备数据

下面示例中使用的数据采集自链家网的真实成交数据。
数据下载地址:
https://databook.top/。

导入数据:

import pandas as pd

fp = "D:/data/南京二手房交易/南京建邺区.csv"
df = pd.read_csv(fp)

# 为了简化,只取10条数据来演示导出效果
df = df.head(10)

image.png

3. 行列设置

先看看默认导出
excel
的效果。

output = "d:\data\output.xlsx"
df.to_excel(output, index=None)

image.png
默认导出的样式就是这样,所有单元格都一样,不管单元格的内容是什么。

3.1. 设置自适应

第一步,我们设置内容自适应(
shrink_to_fit
),确保每个单元格中的内容能够完整显示。

from styleframe import StyleFrame, Styler, utils

style = Styler(shrink_to_fit=True)
sf = StyleFrame(df, styler_obj=style)

writer = sf.to_excel(output)
writer.close()

image.png
可以看出,
StyleFrame
的默认导出样式,给有数据的表格加了边框。
使用
shrink_to_fit=True
样式之后,每个单元格的内容可以完整显示了。

3.2. 设置列宽

从上面的效果,我们发现,所有列的宽度是一样的,无论列中的内容有多长。
我们可以设置某些文字内容比较多列更宽一些。

sf.set_column_width_dict(
    {
        "name": 25,
        "positionInfo": 20,
        "advantage": 15,
        "dealCycleDays": 16,
    }
)

image.png
调整之后,内容看起来更清晰了。

3.3. 设置表头,内容

接下来,我们通过
字号

对齐方式

背景色
以及
是否加粗
来区分
表头

内容
部分。

header_style = Styler(
    bg_color="yellow",
    bold=True,
    font_size=12,
    horizontal_alignment=utils.horizontal_alignments.center,
    vertical_alignment=utils.vertical_alignments.center,
)
content_style = Styler(
    shrink_to_fit=True,
    font_size=8,
    horizontal_alignment=utils.horizontal_alignments.left,
)

sf.apply_column_style(sf.columns, content_style)
sf.apply_headers_style(header_style)

image.png
内容更加紧凑了,表头部分也更突出了。

3.4. 设置行间隔颜色

最后,我们在优化下内容显示部分,用不同的背景色区分
奇数行

偶数行

row_style = Styler(
    bg_color="#32CD32",
    shrink_to_fit=True,
    font_size=8,
    horizontal_alignment=utils.horizontal_alignments.left,
)

# 计算要设置背景色的行索引
indexes = list(range(1, len(sf), 2))
sf.apply_style_by_indexes(indexes, styler_obj=row_style)

image.png

4. 样式设置

样式设置主要是
Styler
这个模块提供的功能。
通过
Styler
类提供的接口,我们可以设置灵活的控制导出的样式。

4.1. 字体

我们给第一行设置不同的字体(
font="STKaiti"
),看看导出的效果:

first_line_style = Styler(
    shrink_to_fit=True,
    font="STKaiti",
    font_size=14,
    horizontal_alignment=utils.horizontal_alignments.left,
)
sf.apply_style_by_indexes(indexes_to_style=[0], styler_obj=first_line_style)

image.png
第一行的字体是
华文楷体
,和其他行不一样。

4.2. 颜色

再把第一行的字调成蓝色(
font_color="blue"
)。

first_line_style = Styler(
    shrink_to_fit=True,
    font="STKaiti",
    font_size=14,
    font_color="blue",
    horizontal_alignment=utils.horizontal_alignments.left,
)
sf.apply_style_by_indexes(indexes_to_style=[0], styler_obj=first_line_style)

image.png

4.3. 背景色

再给第一行加一个红色背景(
bg_color="red"
)。

first_line_style = Styler(
    shrink_to_fit=True,
    font="STKaiti",
    font_size=14,
    font_color="blue",
    bg_color="red",
    horizontal_alignment=utils.horizontal_alignments.left,
)
sf.apply_style_by_indexes(indexes_to_style=[0], styler_obj=first_line_style)

image.png

4.4. 边框

边框
是区隔,突出内容的一种手段,比如,我们可以在表头部分用
实线粗边框

border_type=utils.borders.thick
),内容部分用
虚线细边框

border_type=utils.borders.dashed
)。

header_style = Styler(
    bg_color="yellow",
    bold=True,
    font_size=14,
    border_type=utils.borders.thick,
)
content_style = Styler(
    shrink_to_fit=True,
    font_size=12,
    border_type=utils.borders.dashed,
)

sf.apply_column_style(sf.columns, content_style)
sf.apply_headers_style(header_style)

image.png

4.5. 数字和日期

最后,看看如何定制数字(
number_format
)和日期(
date_format
)的显示方式。
我们把上面示例中的总价(
totalPrice
)保留两位小数,日期(
DealDate
)改为只显示月和日。

num_style = Styler(
    shrink_to_fit=True,
    font_size=12,
    number_format=utils.number_formats.general_float,
    border_type=utils.borders.dashed,
    horizontal_alignment=utils.horizontal_alignments.left,
)
sf.apply_column_style(["totalPrice", "unitPrice"], num_style)

date_style = Styler(
    shrink_to_fit=True,
    font_size=12,
    date_format="DD/MM",
    border_type=utils.borders.dashed,
    horizontal_alignment=utils.horizontal_alignments.left,
)
sf.apply_column_style("dealDate", date_style)

image.png

5. 总结

导出分析结果
是我们做数据分析的最后一步,也是最容易被忽视的一步。
我们常常把大部分的精力都会花在数据的整理和分析上,最后给客户提供一个简易的报告和数据。

殊不知,导出一个美观清晰的分析结果和数据,反而更能得到客户的肯定和信任,因为这才是客户能够切身感知到的部分,否则花在数据整理和分析的精力再多,也不能让客户有直接的感受。

上周我们
发布
了与AI应用开发相关的第1个开源小项目 ——
DashScope SDK for .NET
,今天我们再发布一个开源小项目 ——
SemanticKernel.DashScope
,今天这个项目才是主角,因为我们想基于 Semantic Kernel 开发大模型应用。

首先分享几个与 Semantic Kernel 与 DashScope 相关的消息:

  • Java 1.0 Release Candidate for Semantic Kernel
    发布了,SK 不仅是 .NET 版的 AI 应用开发框架,土豪的微软正在同时开发三个技术平台的版本 —— .NET, Java, Python ,为了AI,微软也是够拼。
  • DashScope 开始支持 function calling,2月22日我们提交工单咨询阿里云 DashScope 是否 function calling,阿里云回复说不支持,这周发现开始支持了,阿里云的动作够快。
  • 限时免费的通义千问系列大模型18号要开始收费了,我们现在使用的 qwen-max 就属于限时免费款。

【阿里云】灵积大模型服务平台通义千问系列大模型将于2024年3月18日调整计费。请您查看最新定价计费说明。查看地址:
https://t.aliyun.com/U/4ij1a1
。感谢您的使用

下面简单介绍一下今天的主角,github 仓库地址
https://github.com/cnblogs/semantic-kernel-dashscope

这个开源项目不仅实现了支持
Semantic Kernel
的 DashScope Connector,还实现了支持
Kernel Memory
的 DashScope Extension,所以分成了2个子项目:

下面通过简单的示例代码快速体验一下。

先体验
Cnblogs.SemanticKernel.Connectors.DashScope

安装 nuget 包

dotnet add package Cnblogs.SemanticKernel.Connectors.DashScope

写一个简单的控制台程序与通义千问 qwn-max 大模型进行对话

var builder = Kernel.CreateBuilder();
builder.Services.AddDashScopeChatCompletion(apiKey, "qwen-max");
var kernel = builder.Build();

var prompt = "博客园是什么样网站,用简练的语言回答";
var result = await kernel.InvokePromptAsync(prompt);
Console.WriteLine(result);

运行程序,通义千问的回答如下:

博客园是一个面向软件开发和技术爱好者的中文技术博客平台,用户可以在该网站上创建、分享和交流编程知识、技术文章、行业动态等内容。它致力于为程序员及互联网相关人员提供一个高质量的内容创作与分享社区,促进技术信息的传播与交流。

接着体验
Cnblogs.KernelMemory.AI.DashScope

安装 nuget 包

dotnet add package Microsoft.KernelMemory.Core
dotnet add package Cnblogs.KernelMemory.AI.DashScope

写一个简单的控制台程序,使用
园子的第一款简陋鼠标垫,是否是您值得拥有的周边
这篇博文进行 RAG(Retrieval Augmented Generation) 与通义千问大模型进行对话

var memory = new KernelMemoryBuilder()
    .WithDashScopeDefaults(apiKey)
    .Build<MemoryServerless>();

await memory.ImportWebPageAsync("https://www.cnblogs.com/cmt/p/17974346");

var question = "博客园鼠标垫在哪买";
var answer = await memory.AskAsync(question);
Console.WriteLine($"{answer.Result}");

上面的代码很简单,但背后比较复杂,Kernel Memory 做了很多事情,比如
memory.ImportWebPageAsync
方法执行时完成了博文内容的抓取、内容分割(partition)、调用 DashScope api 生成 embedding 向量并保存至向量数据库,从下面的日志可以看出来

info: Microsoft.KernelMemory.Pipeline.BaseOrchestrator[0]
      Queueing upload of 1 files for further processing [request f3626f2e67ad451a959d056b4a9e3fbd202403130328152696707]
info: Microsoft.KernelMemory.Pipeline.BaseOrchestrator[0]
      File uploaded: content.url, 38 bytes
info: Microsoft.KernelMemory.Pipeline.BaseOrchestrator[0]
      Handler 'extract' processed pipeline 'default/f3626f2e67ad451a959d056b4a9e3fbd202403130328152696707' successfully
info: Microsoft.KernelMemory.Pipeline.BaseOrchestrator[0]
      Handler 'partition' processed pipeline 'default/f3626f2e67ad451a959d056b4a9e3fbd202403130328152696707' successfully
info: Microsoft.KernelMemory.Pipeline.BaseOrchestrator[0]
      Handler 'gen_embeddings' processed pipeline 'default/f3626f2e67ad451a959d056b4a9e3fbd202403130328152696707' successfully
info: Microsoft.KernelMemory.Pipeline.BaseOrchestrator[0]
      Handler 'save_records' processed pipeline 'default/f3626f2e67ad451a959d056b4a9e3fbd202403130328152696707' successfully
info: Microsoft.KernelMemory.Pipeline.BaseOrchestrator[0]
      Pipeline 'default/f3626f2e67ad451a959d056b4a9e3fbd202403130328152696707' complete

在通过
memory.AskAsync
方法发送提示词时,不仅包含用户输入的提示词,而且会根据用户输入的提示词调用 DashScope api 生成提示词的 embedding 向量,并且用这个向量在向量数据库中进行语义搜索,将搜索到的结果与用户输入的提示词一起发送给大模型(这就是RAG)。

通义千问 qwen-max 大模型训练时并没有关于博客园鼠标垫的数据,所以如果不进行 RAG,通义千问不会给出想要的答案,

有了 Kernel Memory,使用 RAG 变得很简单,看看下面采用 RAG 的回答效果

博客园鼠标垫可以在淘宝上购买,具体购买方式有两种:

1. 淘宝搜索“博客园”,找到相关店铺和商品链接进行购买。
2. 直接通过提供的淘宝店购买链接购买:https://item.taobao.com/item.htm?id=761724714914

另外,如果不想在淘宝店购买,还可以选择添加园子的企业微信进行购买。

对比一下,不采用 RAG,直接问通义千问

很抱歉,我无法提供最新、实时的购物信息,包括博客园是否售卖鼠标垫以及购买途径。博客园(Cnblogs)是一个面向软件开发者的IT技术博客平台,并非商品销售网站,一般不会直接售卖鼠标垫等实物商品。

如果您想购买鼠标垫,可以尝试在各大电商平台如淘宝、京东、苏宁易购等搜索相关商品进行选购。

RAG的效果很明显。

欢迎大家关注这个开源项目,欢迎反馈,欢迎提交 PR
https://github.com/cnblogs/semantic-kernel-dashscope

接下来我们会尝试实现对 function calling 的支持。

Prompt进阶2:LangGPT(构建高性能Prompt策略和技巧)--最佳实践指南

0.前言

左图右图 prompt 基本是一样的,差别只在提示工程这个词是否用中英文表达。我们看到,一词之差,回答质量天壤之别。为了获得理想的模型结果,我们需要调整设计提示词,这也就是所谓的提示工程。

一个广泛的理解误区就是将 prompt 简单理解为自己精心设计的那一小段提示词,而忽略了prompt 的其他来源 。

我们用输入法做个类比,第一张图片中的 “春眠” 是我们的输入,“不觉晓” 是模型输出,所以 “春眠” 是 Prompt。到了第二张图片, “春眠不觉晓” 是模型输入(“不觉晓”是模型前一轮的输出),“处处闻啼鸟” 是模型输出,所以 “春眠不觉晓” 是 Prompt。prompt 的来源可以是我们的预设 prompt,也可以是用户的输入,甚至模型自己之前的输出也是 prompt。所以,要用好大模型能力,我们应意识到:"所有会被模型用于预测输出结果的内容,都是 Prompt"。

相比一般提示词,将提示词这一作文题变为填空题,编写门槛降低的同时,更加清晰和强大。

1.1 六大提高性能的策略

1.1 策略一:清晰地表达指令

GPT 无法读取您的思维。如果输出过长,要求它提供简短的回复。如果输出过于简单,要求它提供专业级的写作。如果您不喜欢输出的格式,请示范您希望看到的格式。GPT 模型需要猜测的东西越少,您得到想要的结果的可能性就越大。

技巧:

  • 在查询中包含更多细节以获取更相关的回答
  • 要求模型采用特定的角色
  • 使用分隔符清楚地指示输入的不同部分
  • 指定完成任务所需的步骤
  • 提供示例
  • 指定期望的输出长度

1.2 策略二:提供参考文本

GPT 模型可以自信地编造虚假的答案,尤其是在被问到深奥的话题或被要求提供引用和 URL 时。就像考试前准备的笔记本可以帮助学生取得更好的成绩一样,为 GPT 模型提供参考文本也可以减少它编造答案的可能性。

技巧:

  • 指示模型使用提供的参考文本进行回答
  • 指示模型使用参考文本中的引文进行回答

1.3 策略三:将复杂任务分解为更简单的子任务

正如在软件工程中将复杂系统分解为一组模块组件是一个好的实践一样,提交给 GPT 模型的任务也应该进行拆分。复杂的任务错误率往往比简单任务高。此外,复杂任务通常可以重新定义为一系列更简单任务的工作流,其中早期任务的输出被用于构造后期任务的输入。

技巧:

  • 使用意图分类识别与用户查询最相关的指令
  • 对需要非常长对话的对话应用,汇总或过滤之前的对话
  • 分段摘要长文档,递归构建完整摘要

1.4 策略四:给予 GPT“思考”的时间

如果要求您立刻计算 17 乘以 28,您可能不会立即知道答案,但仍能通过思考得出正确解。类似地,GPT 模型如果被要求立即回答也更容易出现推理错误,而逐步思考问题可以更可靠地得出正确答案。

技巧:

  • 指示模型在匆忙得出结论之前自己推导出解决方案
  • 使用内心独白或一系列查询来隐藏模型的推理过程
  • 询问模型是否在之前的轮次中遗漏了任何信息

1.5 策略五:使用外部工具

补充 GPT 模型的输入短板,为其提供其他工具的输出。例如,文本检索系统可以告知 GPT 模型相关文档的信息。如果一项任务可以由工具而不是 GPT 模型更可靠、更高效地完成,则将其外包可以取得两全其美的效果。

技巧:

  • 使用基于嵌入的搜索实现高效的知识检索
  • 使用代码执行进行更准确的计算或调用外部 API
  • 允许模型访问特定函数

1.6 策略六:系统地测试更改

有时候很难分辨出某项更改(如新指令或设计)是否提升了系统性能。查看几个例子可能会提示哪种方法更好,但样本量太小时,难以区分真正的改进与随机幸运。也许某项更改对某些输入有益,但对其他输入有害。

评估程序(或“测评”)对优化系统设计很有用。好的测评应该具有以下特点:

  • 代表真实使用情况(或至少包含多样性)
  • 包含大量测试用例以增强统计功效(参见下表)
  • 易于自动化或重复
需检测的差异 需样本量(95%置信度)
30% ~10
10% ~100
3% ~1000
1% ~10000

模型输出的评估可以由计算机、人类或两者的组合来完成。计算机可以用客观标准(如只有单一正确答案的问题)自动进行测评,也可以用其他模型查询对一些主观或模糊标准进行测评。
OpenAI Evals
是一个开源软件框架,提供了创建自动化测评的工具。

对于输出范围广泛且同等高质量的问题(如需要长答案的问题),基于模型的测评可发挥作用。模型测评与需要人工评估的问题界限模糊,随着模型变得越来越强大而不断扩展。我们鼓励您进行试验,了解基于模型的测评对您的用例能发挥多大作用。

技巧:

  • 根据参考标准答案评估模型输出

2.技巧深入

上述每种策略都可以用具体的技巧来实现。这些技巧旨在提供思路,绝非面面俱到,您应随意尝试这里未涉及的创造性思路。

2.1 策略一:清晰地表达指令

技巧:在查询中包含更多细节以获取更相关的回答

为了获得高度相关的回复,查询需要提供任何重要的细节或上下文信息,否则就要由模型猜测您的意图。

劣质 优质
Excel 中如何相加数字? 在 Excel 中如何对一行金额自动相加?我想对整个工作表的行进行求和,总和显示在右边一列"总计"中。
谁是总统? 2021年墨西哥总统是谁,选举频率是多久一次?
编写代码计算斐波那契数列。 用 TypeScript 编写一个函数来有效地计算斐波那契数列。对代码添加详细注释以解释每个部分的作用及编写方式。
摘要会议记录。 用一段话摘要会议记录。然后用 markdown 列表罗列发言人及每个人的要点。最后,列出会议记录中发言者提出的后续步骤或行动事项(若有)。

技巧:要求模型采用特定的角色

系统消息可用于指定模型在回复时采用的角色。

技巧:使用分隔符清楚地指示输入的不同部分

三引号、XML标签、标题等分隔符可以帮助标记需不同对待的文本片段。

对于简单的任务,使用分隔符可能不会影响输出质量。然而,任务越复杂,明确任务细节就越重要。不要让 GPT 模型费劲理解您到底在要求什么。

技巧:指定完成任务所需的步骤

有些任务最好用一系列步骤来表达。明确写出步骤可以让模型更容易遵循。

技巧:提供示例

在所有示例上演示任务的各种变化通常比仅给出说明更高效,但在某些情况下提供示例可能更简单。例如,如果您希望模型模仿回复用户查询的特定风格,而这种风格难以明确表达。这被称为 “few-shot” 提示。

技巧:指定期望的输出长度

您可以要求模型生成特定目标长度的输出。目标输出长度可以以单词数、句子数、段落数、项目符号数等来指定。但是,指示模型生成特定数量的单词不能高精度实现。模型可以更可靠地生成特定段落数或项目符号数的输出。

2.2 策略二:提供参考文本

技巧:指示模型使用提供的参考文本进行回答

如果我们能为模型提供与当前查询相关的可信信息,那么我们可以指示模型使用提供的信息来组成其回答。

鉴于 GPT 模型具有有限的上下文窗口,为了应用此技巧,我们需要某种方法根据问题动态查找相关信息并添加到模型输入中。
嵌入
可用于实现高效的知识检索。请参阅技巧“使用基于嵌入的搜索实现高效知识检索”以获取更多详细信息。

技巧:指示模型使用参考文本中的引文进行回答

如果输入补充了相关知识,则可以轻松要求模型在答案中添加引文,引用提供文档的相关段落。输出中的引文然后可以通过与提供文档的字符串匹配来程序化验证。

2.3 策略三:将复杂任务分解为更简单的子任务

技巧:使用意图分类识别与用户查询最相关的指令

对于需要大量独立指令集来处理不同情况的任务,首先对查询进行分类以确定所需指令会很有益。这可以通过定义固定类别并针对每个类别硬编码相关指令来实现。该过程也可以递归应用以将任务分解成一系列阶段。这种方法的优点是每个查询只包含执行任务下一阶段所需的指令,这可以与使用单个查询执行整个任务相比降低错误率。这也可以降低成本,因为更大的提示需要更高的运行成本(
查看价格信息
)。

例如,对于客户服务应用,查询可以划分为以下类别:

根据客户查询的分类,可以为 GPT 模型提供更具体的指令来处理后续步骤。例如,假设客户需要“故障排除”的帮助。

注意,模型被指示在对话状态发生变化时输出特殊字符串。这使我们可以将系统转化为状态机,其中状态确定注入哪些指令。通过跟踪状态、该状态下相关的指令,以及从该状态允许的状态转移,我们可以为用户体验设置更难以用不太结构化的方法实现的约束。

技巧:对需要非常长对话的对话应用,汇总或过滤之前的对话

由于 GPT 模型具有固定的上下文长度,因此将整个对话包含在上下文窗口内的用户与助手之间的对话无法无限期地继续。

这一问题有多种解决方法,一种是在输入大小达到预定阈值长度时,使用一个查询来总结对话的一部分,并将之前对话的摘要包含在系统消息中。或者,可以在整个对话的过程中异步地在后台总结之前的对话。

另一种解决方案是动态选择与当前查询最相关的之前对话部分。请参阅技巧“使用基于嵌入的搜索实现高效知识检索”。

技巧:分段摘要长文档,递归构建完整摘要

由于 GPT 模型具有固定的上下文长度,它们无法在单个查询中概括比上下文长度减去生成摘要长度还要长的文本。

要摘要像书籍这样的非常长文档,我们可以使用一系列查询逐部分摘要文档。部分摘要可以连接在一起生成摘要的摘要。该过程可以递归进行,直到整个文档被摘要。如果为了理解后面部分需要使用前面部分的信息,则可以在摘要某点内容时,将该点之前文本的运行摘要也包括在内,这一技巧也很有用。OpenAI 之前的
研究
已经研究了使用 GPT-3 变体递归摘要书籍的效果。

2.4 策略四:给予 GPT“思考”的时间

技巧:指示模型在匆忙得出结论之前自己推导出解决方案

有时候明确地让模型先自行推理出答案再回复用户,可以获得更好的效果。

例如,我们想让模型评判一个学生对数学问题的解答。 最直接的方法是简单地询问模型该解答是否正确。

但如果我们首先让模型自己解出题目并生成答案,再让其比较学生的答案,就更容易发现问题:

SYSTEM: 首先自己解出这道题并生成答案。然后再比较你的答案和学生的答案,判断学生的答案是否正确。在比较之前,不要依赖学生的答案来决定其是否正确。

USER: <插入题目>

<插入学生答案>

ASSISTANT: <模型先生成自己的答案> 

<模型比较两个答案并作出评价>

技巧:使用内心独白或一系列查询来隐藏模型的推理过程

有时候我们不想让用户看到模型的全部推理过程,只需要展示最后的回复。

这时可以使用"内心独白"把需要隐藏的部分放入特定格式中,在展示给用户前把这些部分删除。

或者,可以进行一系列查询,其中只有最后一个查询的输出会展示给用户。

#隐藏查询1: 仅提供问题描述,让模型解出答案

#隐藏查询2: 提供问题描述、模型解答和学生解答,让模型判断学生解答的正确性

#可见查询: 假设模型是一个有帮助的导师,如果学生错误,给出提示;如果正确,给予鼓励

技巧:在前几轮后询问模型是否遗漏了相关信息

当我们要求模型列举某文本中的所有相关摘录时,模型常会过早停止而未罗列全部。

这时可以在前几轮查询后,再询问模型是否还遗漏了相关内容,以获取更完整的结果。

2.5 策略:使用外部工具

技巧:使用基于嵌入的搜索实现高效的知识检索

我们可以在模型的输入中加入相关的外部信息,帮助其生成更准确的回复。

例如,如果用户询问一个具体的电影,将该电影的高质量信息(演员、导演等)也加入输入中会很有用。

通过使用文本嵌入可以实现高效的知识检索,从而动态地在运行时将相关信息加入模型输入中。

具体实现可以参考
OpenAI Cookbook

技巧:使用代码执行进行更精确的计算或调用外部 API

GPT 模型自己进行算术运算或长时间计算时误差很大。这时可以让其编写代码进行运算并运行。

代码执行也可以用来调用外部 API。如果事先教会模型如何使用某 API,它可以编写调用该 API 的代码。

需要注意,运行模型生成的代码有安全风险,需要采取防护措施。

技巧:系统地测试各种改变

有时候很难判断某项改变是否提升了系统性能。仅观察几个例子来判断是不可靠的。

适当的评估流程对优化系统设计非常有用。好的评估应该具有代表性、包含足够多的测试用例。

评估可以由计算机、人工或两者混合来进行。计算机可以自动进行客观的评估。OpenAI Evals 提供了用于构建自动评估的开源工具。

当存在多种可被认为同等优质的输出时,使用模型进行评估也可行。我们鼓励针对不同使用案例进行试验,衡量模型评估的可行性。

技巧:参照标准答案来评估模型输出

如果问题的正确答案需要包含某些已知的事实,我们可以用模型查询来统计答案中包含了多少需要的事实。

例如:

SYSTEM: 检查提交的答案是否直接包含下列信息:

- Neil Armstrong 是第一个登月的人
- Neil Armstrong 首次登月的日期是 1969年7月21日

对于每个要点:

1 - 重述要点
2 - 提供与要点最相关的答案摘录
3 - 分析仅看摘录的人是否可以推断出该要点,解释你的推理过程
4 - 如果答案是肯定的,写 "yes",否则写 "no"

最后,统计"yes"的个数,用以下格式提供:

{"count": <yes的个数>}

如果答案满足所有要点,count 将为要点的总数。如果仅满足部分要点,count 将小于要点总数。

这样可以构建不同的模型评估变体来统计答案与标准答案的重合情况,以及是否存在矛盾。

更多优质内容请关注公号:汀丶人工智能;会提供一些相关的资源和优质文章,免费获取阅读。

  • 参考链接:

https://github.com/EmbraceAGI/LangGPT/tree/main

https://github.com/EmbraceAGI/LangGPT/blob/main/Docs/GPTBestPractise.md


大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是
使用恩智浦GUI Guider快速创建全新LCD屏示例工程的步骤

在痞子衡旧文
《在i.MXRT1170上快速点亮一款全新LCD屏的方法与步骤》
里,痞子衡介绍了在官方 SDK 裸机驱动 elcdif 示例工程基础上做修改以支持一款全新 LCD 屏的方法,那篇文章主要涉及得是对于 LCD 驱动 IC 的底层适配改动。今天我们在其基础之上尝试用恩智浦 GUI Guider 工具快速创建这款 LCD 屏(深圳柯达科电子生产的 KD050FWFIA019-C019A)的 LVGL 框架下的示例工程。

一、GUI Guider简介

LVGL 是一个适用于各种嵌入式平台和显示器的开源且免费的 GUI 框架库,其第一个版本于 2016 年开源在 github 上。我们知道嵌入式 GUI 应用开发很大的工作量在于 UI 界面的设计,一个成熟的 GUI 框架通常都自带配套的 UI 界面编辑工具,但是 LVGL 在 UI 工具方面是比较匮乏的,直到 2022 年初官方才找了家第三方公司合作推出了
SquareLine Studio
,并且不幸得是 2024 年 2 月,LVGL 已经结束了和 SquareLine Studio 的合作。

恩智浦作为 LVGL 官方合作伙伴,在 LVGL 的支持上投入了不少精力,正因为官方 UI 工具的匮乏,恩智浦便开发了 GUI Guider 工具(第一个版本发布于 2021 年初),借助 GUI Guider 的拖放编辑器可以轻松使能 LVGL 的众多特性,如小部件、动画和样式来创建 GUI,而只需少量代码或根本无需任何代码。

GUI Guider 支持了众多恩智浦 MCU 开发板以及一些官方指定的 LCD 屏,其生成的示例工程底层代码也是基于官方 SDK。其软件版本对应关系可以在工具菜单栏 Help->Release Note 下找到:

二、GUI Guider下创建工程

痞子衡安装得是最新的 GUI Guider v1.7.0-GA,让我们打开这个工具创建初始工程,前两步分别是选 LVGL 版本(v7/v8)以及 MCU 板卡(如果是自制板卡,只需要选对 MCU 型号即可),这里痞子衡选择了 LVGL v8 以及 MIMXRT1170-EVKB。

第三步选择模板的时候,有4种选择,
EmptyUI
就是纯空界面,
Widget
是一些典型的简单界面模板,
Application
是一些复杂的综合应用界面模板,
Local Projects
是导入本地已有的用户工程界面模板。因为只是做一个简单示例,痞子衡这里选择了 EmptyUI:

第四步(也是创建工程的最后一步)是设置 LCD 屏属性,因为我们用得是新屏,所以在 Panel Type 里需要选择 Custom,然后手动输入屏的名称(会在代码里生成相应宏 DEMO_PANEL_KD050FWFIA019)以及尺寸,最后点击 Create 按钮便能生成初始工程。

  • Note: 这里设置的 LCD 屏尺寸(宽高)在工程生成之后无法再更改。

三、GUI Guider里设计界面

现在来到了 GUI 设计主界面,因为只是简单示例,我们随便拖动一个 Label 控件,里面写入 Hello World 就算设计好界面了。界面设计完成需要在右上角点击 Generate Code -> C 以生成相应界面构建 C 代码。

四、修改MCU工程底层驱动

现在让我们打开创建 GUI 工程指定的文件路径,找到如下工程目录,rt1170_qsg_proj.guiguider 便是 GUI 设计工程文件,一级目录下都是跟 GUI 相关的文件夹(\generated 存放工具生成的相关字库控件资源文件,\custom 里可以自行添加用户界面响应相关逻辑代码),\sdk 文件夹下则是最终 MCU 工程(MCUX/IAR/MDK/GCC 均支持)。

因为 GUI Guider 已经将所需的源文件全部聚合在这个目录下,所以我们不用额外再下载任何软件包。这时候你可以选择你擅长的 IDE 去做进一步开发,痞子衡用 IAR 比较多, 所以便打开了 \GUI-Guider-Projects\rt1170_qsg_proj\sdk\iar\lvgl_guider_cm7.eww 工程。

  • Note:除了 MCUX 的工程可以在导入时另选官方 SDK 版本,其余 IDE 的 SDK 版本均是确定的。如需其它 SDK 版本,需要手动更新 \GUI-Guider-Projects\rt1170_qsg_proj\sdk\Core 里文件。

当前 GUI Guider 生成的 MCU 工程均是 FreeRTOS 版本,没有提供裸机版本。无论工程是否基于 RTOS,这并不影响我们移植全新 LCD 屏的驱动。按照痞子衡旧文
《在i.MXRT1170上快速点亮一款全新LCD屏的方法与步骤》
里的步骤,将对 elcdif_support.c/h 的改动全部放到 display_support.c/h 里,再添加 fsl_ili9806e.c/h 文件到 lvgl_guider_cm7 工程里便大功告成了。此外由于那篇文章只是点屏,没有涉及触摸 IC 的驱动,所以这里暂时也不用考虑使能触摸功能,可将 DEMO_InitTouch() 函数的调用先注释掉。

\GUI-Guider-Projects\rt1170_qsg_proj\sdk\Core\board\display_support.c
\GUI-Guider-Projects\rt1170_qsg_proj\sdk\Core\board\display_support.h
\GUI-Guider-Projects\rt1170_qsg_proj\sdk\Core\video\fsl_ili9806e.c
\GUI-Guider-Projects\rt1170_qsg_proj\sdk\Core\video\fsl_ili9806e.h

给板卡供电,连接上新屏 KD050FWFIA019,编译下载修改后的 lvgl_guider_cm7 工程,这时候就能看到 LVGL 版本的 hello world 正常显示了:

至此,使用恩智浦GUI Guider快速创建全新LCD屏示例工程的步骤痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的
博客园主页

CSDN主页

知乎主页

微信公众号
平台上。

微信搜索"
痞子衡嵌入式
"或者扫描下面二维码,就可以在手机上第一时间看了哦。

前言

你是否想在使用 GoFrame 的过程中,拥有一个能打印异常堆栈,能自定义响应状态码,能统一处理响应数据的接口。如果你回答是,那么,请耐心看完本文,或许会对你有所启发。若文中由表达不当之处,恳请不吝赐教。

异常都需要错误堆栈吗

为什么会问这个问题呢,所有的接口错误都会向日志中抛出堆栈信息,这不是好事吗?答案是否定的。

业务开发中,通常有
业务异常

系统异常
两种
err
,我这里暂且这么称呼,也有的称为业务异常为"错误",系统异常为"异常"。业务异常是由用户输入不当引起的,比如说账号密码错误,这种
err
通常只返回给用户即可,
不需要打印堆栈信息
。而系统异常是由系统内部自发引起的,比如说
SQL
语句不当,这种错误需要打印堆栈信息,且不能把
err
返回到用户那里,不然会暴露代码结构,严重的可能会暴露数据库结构。


GoFrame
中,因为有着强大的
gerror
组件,所以只要接收了任何组件方法中的
err
,不论
业务异常

系统异常
,都会打印堆栈信息,这与我们的设计目标不符合,需要解决它。

状态码

此处的状态码区别与 HTTP 状态码,它是我们自定义的一套业务码,比如这样:

{
	"code": 10001,
	"message": "用户名密码错误",
	"data": null
}

{
	"code": 10002,
	"message": "用户不存在",
	"data": null
}

它们的 HTTP 状态码都是 200,代表响应成功,但是业务状态码不同,用以区分不同的业务异常。

一个例子

我们来编写一个完整的示例:

接口文件:/api/exception/v1/exception.go:

// 模拟业务异常
type BusinessReq struct {
	g.Meta `path:"/business" method:"get"`
}

type BusinessRes struct {
	Name string
	Age  int
}

// 模拟系统异常
type SystemReq struct {
	g.Meta `path:"/system" method:"get"`
}

type SystemRes struct {
	Name string
	Age  int
}

控制器文件:/internal/controller/exception/exception_v1_*.go:

func (c *ControllerV1) Business(ctx context.Context, req *v1.BusinessReq) (res *v1.BusinessRes, err error) {
	err = service.Exception().Business()
	if err != nil {
		return nil, err
	}
	return &v1.BusinessRes{
		Name: "business",
		Age:  1,
	}, nil
}

func (c *ControllerV1) System(ctx context.Context, req *v1.SystemReq) (res *v1.SystemRes, err error) {
	err = service.Exception().System()
	if err != nil {
		return nil, err
	}
	return &v1.SystemRes{
		Name: "system",
		Age:  1,
	}, nil
}

服务文件:/internal/logic/exception/exception.go:

func (s *sException) Business() error {
	return gerror.New("用户名密码错误")
}

// System 这里我们对 gjson.Decode() 传入错误数据,用来模拟组件内部抛出err
func (s *sException) System() error {
	_, err := gjson.Decode("")
	if err != nil {
		return err
	}
	return nil
}

这个例子模拟了一个完整的接口,从
api

controller

logic
,然后我们请求它们,分别从响应信息和控制台两个角度看看它们的结果。

业务异常 Business

curl http://127.0.0.1:8000/business

控制台:

接口响应:

{
	"code": 50,
	"message": "用户名密码错误",
	"data": null
}

系统异常 System

curl http://127.0.0.1:8000/system

控制台:

接口响应:

{
	"code": 50,
	"message": "json Decode failed: EOF",
	"data": null
}

优化方案

此时,我们的接口中有三个不足:

  1. 业务异常不应该抛出堆栈,因为用户名或密码错误的堆栈没有意义;
  2. 系统异常的响应信息中,
    message
    不应该抛出 "json Decode failed: EOF",应该使用
    未知错误
    或者
    系统错误
    这类字眼;
  3. 业务异常和系统异常的业务码,也就是响应信息中的
    code
    ,不应该都使用 50,应当做以区分。

设计统一 err

在 GoFrame 的工程目录中,有一个包
/internal/packed
,我们可以在此处编写我们自己的
err
处理,后面的代码可以做为参考,也可以直接复制过去用:

/internal/packed/err.go:

type pErr struct {
	maps map[int]string
}

var Err = &pErr{
	maps: map[int]string{
		0:     "请求成功",
		10001: "用户名或密码错误",
		10002: "用户不存在",
		99999: "未知错误",
	},
}

// GetMsg 获取code码对应的msg
func (c *pErr) GetMsg(code int) string {
	return c.maps[code]
}

// Skip 抛出一个业务级别的错误,不会打印错误堆栈信息
func (c *pErr) Skip(code int, msg ...string) (err error) {
	var msgStr string
	if len(msg) == 0 {
		msgStr = c.GetMsg(code)
	} else {
		msg = append([]string{c.GetMsg(code)}, msg...)
		msgStr = strings.Join(msg, ", ")
	}
	// NewWithOption 在低版本的 gf 上不存在,请改用 NewOption
	return gerror.NewWithOption(gerror.Option{
		Stack: false,
		Text:  msgStr,
		Code:  gcode.New(code, "", nil),
	})
}

// Sys 抛出一个系统级别的错误,使用code码:99999,会打印错误堆栈信息
// msg 接受string和error类型
// !!! 使用该方法传入error类型时,一定要注意不要泄露系统信息
func (c *pErr) Sys(msg ...interface{}) error {
	var (
		code     = 99999
		msgSlice = []string{
			c.GetMsg(code),
		}
	)

	if len(msg) != 0 {
		for _, v := range msg {
			switch a := v.(type) {
			case error:
				msgSlice = append(msgSlice, a.Error())
			case string:
				msgSlice = append(msgSlice, a)
			}
		}
	}

	msgStr := strings.Join(msgSlice, ", ")
	return gerror.NewCode(gcode.New(code, "", nil), msgStr)
}

统一响应数据中间件

设计统一响应数据的中间件,并且注入到 HTTP 请求流程中:

/internal/logic/middleware/response.go

type sMiddleware struct {
}

func init() {
	service.RegisterMiddleware(New())
}

func New() *sMiddleware {
	return &sMiddleware{}
}

type Response struct {
	Code    int         `json:"code"    dc:"业务码"`
	Message string      `json:"message" dc:"业务码说明"`
	Data    interface{} `json:"data"    dc:"返回的数据"`
}

func (s *sMiddleware) Response(r *ghttp.Request) {
	r.Middleware.Next()

	if r.Response.BufferLength() > 0 {
		return
	}

	// 先过滤掉服务器内部错误
	if r.Response.Status >= http.StatusInternalServerError {
		// 清除掉缓存区,防止服务器信息泄露到客户端
		r.Response.ClearBuffer()
		r.Response.Writeln("服务器打盹了,请稍后再来找他!")
	}

	var (
		res  = r.GetHandlerResponse()
		err  = r.GetError()
		code = gerror.Code(err)
		msg  string
	)

	if err != nil {
		msg = err.Error()
	} else {
		code = gcode.CodeOK
		msg = packed.Err.GetMsg(code.Code())
	}

	r.Response.WriteJson(Response{
		Code:    code.Code(),
		Message: msg,
		Data:    res,
	})
}

/internal/cmd/cmd.go

s.Group("/", func(group *ghttp.RouterGroup) {
	group.Middleware(service.Middleware().Response)
	group.Bind(
		exception.NewV1(),
	)
})

结果

然后在服务文件中调用
packed/err

/internal/logic/exception/exception.go:

func (s *sException) Business() error {
	return packed.Err.Skip(10001)
}

// System 这里我们对 gjson.Decode() 传入错误数据,用来模拟组件内部抛出err
func (s *sException) System() error {
	_, err := gjson.Decode("")
	if err != nil {
		return packed.Err.Sys("可选自定义信息")
	}
	return nil
}

结果展示:

Business
{
	"code": 10001,
	"message": "用户名或密码错误",
	"data": null
}

System
{
	"code": 99999,
	"message": "未知错误, 可选自定义信息",
	"data": null
}

用户名或密码错误的业务异常也不会再抛出堆栈异常了:

尾声

上述的代码例子已经开源在:
Github

本博客源码使用的也是这种
err
设计,想要了解更多可以查看:
Github/oldme-api