用 C++构建自己的 GPT 文档工具
虽然通过 Web 界面使用 ChatGPT 是一回事,但创建自己的自主 AI 工具,并通过其 API 与 ChatGPT 交互,则完全是另一回事,特别是当你的目标是保持对用户交互的完全控制时。与此同时,作为一名坚定 C++ 的支持者,我们相信用 C++ 编写的 GPT 工具能减轻处理(无休止的)编辑批注这一艰巨任务所带来的痛苦。
我们旨在探索 MS Office 的自动化领域,并利用 ChatGPT API 增强编辑过程。我们设想了一个复杂的工具,可以将 C++ 与 ChatGPT API 无缝地集成,从而提供一种与 Word 文档中的编辑批注进行交互的新方法。
传统的文档编辑包括手动审阅内容和向特定部分添加批注。就我们而言,当我们编写 C++ 书籍时,我们每次都会遇到 100 多条编辑批注,其中大部分与出版商的风格指南和注释有关。如果能有一种方法将这些批注和相关文本存储在数据库中,那就太好了,更不用说基于人工智能的编辑潜力了。这正是我们的软件所要实现的目标:通过自动化这一过程,我们可以加快编辑工作流程。虽然这个工具可以作为概念验证(POC),不建议用于编写和编辑整本书,但它仍然是一个令人兴奋的自动化练习,当然值得一试。
工作流程从我们的软件扫描 Word 文件开始,使用 Office Automation API 仔细检查文档中嵌入的每一条编辑批注。
枚举完所有批注后,我们的工具就会提取它们以及与之相关的文本段,并将它们存储在 sqlite3 数据库中。在此基础上,它将围绕如何改进或修复文本的特定部分来为 ChatGPT 准备有针对性的问题。通过利用 ChatGPT API,我们可以利用语言模型的丰富知识和语言能力来获取专家的意见和建议。
在收到 ChatGPT 的回复之后,我们的工具会动态地将建议的编辑内容合并到相关的文本片段中,从而根据模型的见解无缝地增强内容。
这种自动化的编辑过程大大减少了手工工作量,并加快了文档的整体细化完善。我们的工具甚至可以跟踪更改,但要记得在完成后关闭“跟踪更改”。
在编程方面,我们的项目中有几个构建块,其中一些可以扩展或替换以满足不同的目的。我们将我们的代码称为概念验证( Proof of Concept, POC)。
以下是这一过程的参与者——我们的构建块:
我们的工具通过使用各种参数和方法来与 ChatGPT 进行接口调用和交互。我们准备要发送给 API 的有效负载并解析响应。要使用我们的工具,必须要获取一个 API 密钥并将其添加到我们的代码中,注意不是“
使用 API 的优势包括:能够与 Chat GPT 进行接口调用和交互,并使用不同的参数和方法,准备要发送到 API 的有效负载,以及解析返回给我们的响应。
使用 ChatGPT API 时,需要考虑以下几点。
为了本文的目的,我们创建了一个通用函数。该函数是模块化的,因为它能生成具有模块化属性和参数的请求,格式如下:
data =
{
{"messages", json::array({{ {"role", "user"}, {"content", entire_converstaion_string} }})},
{"model", model},
{"temperature", temperature},
{"max_tokens", max_tokens},
{"n", n},
{"stop", stop}
};
让我们来看看这些属性,并讨论下它们存在的问题及需求:
“messages”——定义用户和模型之间的对话历史。对话中的每条消息都由两个属性组成:“role”(可以是“system”、“user”或“assistant”)和“content”(消息的实际文本内容)。为了本文的目的,我们使用了“user”
“model”——允许指定想要使用的 ChatGPT 模型版本。在本文中,我们使用了“gpt-3.5-turbo”
“temperature”——可以设置它来控制生成的文本和 prompt 之间的相似程度。例如,高温值可用于生成与 prompt 更不同的文本,而低温值可用于生成与 prompt 更相似的文本。在目标为生成与给定输入相似但具有一定程度的变化或“创造性”文本的情况下,这可能很有用。
“max_tokens”——是每个请求使用的最大 token 数。处理的 token 数量取决于输入和输出文本的长度。
1-2 句~= 30 个 token
1 段~=100 个 token
1,500 个单词~=2048 个 token
作为 ChatGPT API 的用户,我们需要为我们消耗的 token 付费。
“n”——控制模型应提供的响应数量;默认情况下,它被设置为 1,即单个响应
“stop”——表示应触发模型停止生成其响应的字符串。默认情况下设置为换行符。这意味着,当模型在其输出中遇到新行时,它将在那之后停止生成。
我们总是喜欢说,结构良好的 prompt 的重要性是怎么强调也不为过的。精心构建的 prompt 可以作为指导蓝图,影响生成的输出质量。在本文中,我们将深入研究有效 prompt 的组成部分,并提供实际的示例和指导,帮助 C++ 学生在项目中最大限度地发挥 ChatGPT API 的潜力。
下面是一个例子:
// 为GPT请求设置prompt
wstring prompt{ L"I will send you some text, and an associated comment that tells what changes need to be made in the text. Please start your response with 'Changed text: ' followed by the actual updated text. Here is the original text: '" };
prompt += rangeText;
prompt += L"'. And here is the associated comment suggesting the change: '";
prompt += commentText;
prompt += L"'. Please do not respond with anything else, only include the changed text and nothing else. If you do not have the correct answer or don't know what to say, respond with these exact words: 'I do not understand";
//
在编写 prompt 时,最好创建一个模板,其中包含将在整个程序中使用的请求的常量部分,然后根据当前需要更改可变部分。以下是一个良好的 prompt 的一些关键组成部分:
上下文:
上下文作为 prompt 的基础,能提供关键的背景信息。它使语言模型能够掌握任务的本质。无论是简明扼要的问题描述还是相关细节的总结,对提供上下文都至关重要。
示例:
“你是一名软件开发人员,正在为外卖服务开发移动应用程序。该应用程序旨在为用户提供从当地餐馆订餐的无缝体验。作为开发过程的一部分,你需要帮助生成有关该应用程序的功能是如何吸引人的信息丰富内容。”
任务:
任务定义了 prompt 的精确目标或目的。它应该清晰、简洁,并重点关注于 ChatGPT 模型预期的具体信息或操作。
示例:“写一个简短的段落,突出应用程序的主要功能,并展示它们是如何增强客户的送餐体验的。”
约束条件:
约束为 prompt 设置了边界或限制。它们可能包括特定的要求、对响应长度或复杂性的限制或任何其他相关约束。通过定义约束,可以引导生成的输出满足所需的结果。
示例:
“回答应该简明扼要,字数不超过 150 字。重点关注应用程序区别于竞争对手的最突出功能,并使其对用户友好。”
补充说明:
在本节中,你将有机会提供补充上下文或指定所需的输出格式。这可以包括有关预期输入格式或请求以特定格式(如 Markdown 或 JSON)输出的详细信息。
示例:“请将响应格式化为 JSON 对象,其中包含每个特性描述的键值对。每个键都应代表一个特性,其对应的值应提供一个简短的描述,突出其优点。”
通过理解和实现这些基本组件,C++ 开发人员可以掌握构建有效 prompt 的艺术,以便在项目中最优地利用 ChatGPT API。深思熟虑地结合上下文,定义明确的任务,设置约束并提供额外的说明将使开发人员能够获得精确且高质量的结果。
在大多数情况下,我们希望能从你上次结束的地方继续对话。Chat GPT API 使用了一个特殊的标志来实现这一点。如果未设置,将会发生如下的情况:
➢ 法国的首都是哪里?
Request payload: '{"messages":[{"content":"what is the capital of france?","role":"user"}],"model":"gpt-3.5-turbo"}'
Callback JSON: '{"id":"chatcmpl-7AlP3bJX2T7ibomyderKHwT7fQkcN","object":"chat.completion","created":1682799853,"model":"gpt-3.5-turbo-0301","usage":{"prompt_tokens":15,"completion_tokens":7,"total_tokens":22},"choices":[{"message":{"role":"assistant","content":"The capital of France is Paris."},"finish_reason":"stop","index":0}]}
你人工智能朋友的回答是:
➢ 法国的首都是巴黎。
接下来的一个问题是:
➢ 它有多大?
Request payload: '{"messages":[{"content":"How big is it?","role":"user"}],"model":"gpt-3.5-turbo"}'
Callback JSON: '{"id":"chatcmpl-7AlPAabscfyDrAV2wTjep0ziiseEB","object":"chat.completion","created":1682799860,"model":"gpt-3.5-turbo-0301","usage":{"prompt_tokens":13,"completion_tokens":20,"total_tokens":33},"choices":[{"message":{"role":"assistant","content":"I apologize, but I need more context to accurately answer your question. What are you referring to?"},"finish_reason":"stop","index":0}]}
➢ 我很抱歉,但是我需要更多的上下文来准确回答你的问题。你指的是什么?
要解决这一问题,我们需要保持连续的聊天,但我们该如何做到这一点呢?事实上,要做到这一点的唯一方法是必须来回传递一个包含整个对话的字符串。
string entire_converstaion_string;
我们还定义了:
using Conversation = vector<SingleExchange>;
它的定义是:
using SingleExchange = pair<string, string>;
在我们的源代码中,你可以看到我们是如何将 Conversation 对象维护成固定长度的(很明显,我们无法存储无休止的对话)。这个固定长度是在这里设置的:
int conversation_exchange_limit{ 100 };
如前所述,我们的 prompt 在请求的效率中起着关键作用,当涉及到连续聊天时,我们可能需要使用不同的 prompt:
string prompt_start{ "You are a chatbot. I want to have a conversation
with you where you can remember the context between multiple requests. To do
I will send all previous request prompts and your corresponding
please use those as context. All the previous request prompts and
the current will have the 'request: ' before it, and all your corresponding
responses will have 'response: ' before it. Your job is to respond to only the
latest request. Do not add anything by yourself, just respond to the latest
Starting now\n\n" };
当你问你的人工智能朋友:
➢ 给我写一段 C++ 代码,实现从 1 到 10 的计数。
你可能会得到这样的结果:
➢ 当然可以,下面是从 1 到 10 计数的 C++ 代码:
没有任何源代码。
原因如下:发送给 API 的 stop 参数让模型知道它应该在输出的哪个点上停止生成更多内容。当没有指定任何内容时,换行符就是默认值,这意味着模型在输出第一个换行符后就停止生成更多的输出。
但是,如果你将“stop”参数设置为空字符串,你将得到完整的响应,其中将包含源代码:
OLE 自动化是微软在过去引入的一项技术,此后不断发展。在我们的实现中,我们直接使用了 Microsoft 自动化,绕过了 MFC(Microsoft Foundation Classes,微软基础类库)的使用。为了访问 MS Word 的各种元素,如文档、活动文档、批注等,我们为需要交互的每个对象定义了 IDispatch COM 接口。
我们的工具自动化了 MS Word 中的各种任务和特性。它可以读取批注、查找相关文本、打开 / 关闭“跟踪更改”、在后台工作、替换文本、添加批注、保存结果以及关闭文档。下面是我们所使用的函数的描述:
OLEMethod():一个辅助函数,用于调用 IDispatch 接口上的方法,处理方法调用并返回指示错误的 HRESULT 值。
Initialize():该函数通过创建 Word 应用程序的实例并设置其可见性来初始化 OfficeAutomation 类。它能初始化 COM 库,检索 Word 应用程序的 CLSID,创建应用程序的实例,并设置其可见性。
OfficeAutomation():OfficeAutomation 类的构造函数。它初始化成员变量,并使用 false 调用 Initialize 函数以创建不可见的 Word 应用程序实例。
~OfficeAutomation():OfficeAutomation 类的析构函数。它在此实现中不执行任何操作。
SetVisible():设置活动文档可见性的函数。它使用一个布尔参数来确定文档是否应该可见。它使用 OLEMethod 函数来设置 Word 应用程序的可见性属性。
OpenDocument():打开 Word 文档并设置其可见性的函数。它接受一个指向文档路径和一个用于可见性的布尔参数。如果需要,它会初始化该类,检索 Documents 接口,打开指定的文档,并设置其可见性。
CloseActiveDocument():关闭活动文档的函数。它会保存文档,然后关闭文档。它使用 OLEMethod 函数来调用适当的方法。
ToggleTrackChanges():用于切换活动文档的“跟踪修订”特性的函数。它获取特性的当前状态,并在必要时进行切换。它使用 OLEMethod 函数来访问和修改“TrackRevisions”属性。
FindCommentsAndReply():该函数用于查找活动文档中的所有批注,向 ChatGPT API 发送请求以获取建议,并根据 API 响应更新每个批注的关联文本。它遍历每个批注,检索关联的文本范围,用文本和批注作为上下文向 ChatGPT API 发送 prompt,接收 API 响应,并使用建议的更改更新文本范围。
CountDocuments():该函数用于返回与 OfficeAutomation 类关联的 Word 应用程序中打开的文档数。它检索 Documents 接口并返回计数。
在制定审查批注机制时,我们需要能够枚举所有批注,并区分已处理的批注和未处理的批注。
这可以通过以下方式完成:
bool IsCommentResolved(IDispatch* pComment)
{
// 检查批注是否被解析
VARIANT isResolved;
VariantInit(&isResolved);
HRESULT hr = OLEMethod(DISPATCH_PROPERTYGET, &isResolved, pComment, (LPOLESTR)L"Done", 0);
if (FAILED(hr))
{
ShowError(hr);
return false;
}
bool resolved = (isResolved.vt == VT_BOOL && isResolved.boolVal == VARIANT_TRUE);
return resolved;
}
正如你所看到的,使用 OLEMethod() 和 DISPATCH_PROPERTYGET,我们可以检查属性名“Done”,它表示已处理的批注。
接下来,我们可以枚举文档中的所有批注,并打印每个批注的“已处理”(“Resolved”)状态。
在开始之前,我们不仅要枚举批注,还要枚举与之相关的文本。原因在于批注的最初目的。文档的作者撰写并编辑文档。编辑标记一个片段,可以是一个段落、一个句子甚至是一个单词,并添加一条批注。当我们阅读批注时,我们需要该批注的上下文,而上下文就是那个被标记的片段。
因此,当我们枚举所有批注时,我们不仅要打印批注本身,还要打印与之相关的文本(我们的片段)。
当我们开始检查所有批注时,我们需要声明并初始化 2 个指针:
pComments——指向文档的批注。
pRange——指向文档的内容(包含与批注相关联的文本的段)。
它们两个都需被初始化:
{
VARIANT result;
VariantInit(&result);
m_hr = OLEMethod(DISPATCH_PROPERTYGET, &result, m_pActiveDocument, (LPOLESTR)L"Comments", 0);
if (FAILED(m_hr))
{
ShowError(m_hr);
return m_hr;
}
pComments = result.pdispVal;
}
{
VARIANT result;
VariantInit(&result);
m_hr = OLEMethod(DISPATCH_PROPERTYGET, &result, m_pActiveDocument, (LPOLESTR)L"Content", 0);
if (FAILED(m_hr))
{
ShowError(m_hr);
return m_hr;
}
pRange = result.pdispVal;
}
然后我们就可以开始循环遍历文档中的所有批注了。
你可以在我们的源代码中看到这是如何实现的,但一般来说,我们从批注开始,转到相关的文本,并检查批注是否得到了处理。然后,我们就可以将其打印到报告中,将其添加到数据库中,或者将其发送给 Chat GPT API。
为了通过网络与任何 API 接口,我们使用了通用代码来方便地发送请求并使用 JSON 数据格式解析响应。在此过程中,我们使用了 libCurl,这是一个强大的工具,被广泛用于使用命令行或脚本在网络上传输数据。它在不同领域有着广泛的应用,包括汽车、电视、路由器、打印机、音频设备、移动设备、机顶盒和媒体播放器等领域。它是众多软件应用程序的互联网传输引擎,安装量达数十亿次。
如果你查看了我们的源代码,就可以看到 libCurl 是如何使用的。
通过利用 MS Office 自动化的强大功能并将其与 ChatGPT API 集成,我们使编辑和作者能够简化其工作流程,节省宝贵的时间并提高他们的工作质量。C++ 和 ChatGPT API 之间的协作促进了流畅高效的交互,使我们的工具能够为每个编辑批注提供智能且感知上下文的建议。
因此,我们的小型 MS Office 自动化 POC 工具,由 ChatGPT API 和 C++ 支持,彻底改变了编辑过程。通过自动提取编辑批注,与 ChatGPT 互动以寻求专家指导,并无缝集成编辑建议,我们使用户能够提高他们在 Word 文档中工作的质量和效率。这种强大的技术组合为高效的文档编辑开辟了新的可能性,并代表着 MS Office 自动化领域的重大飞跃。
原文链接:
https://www.infoq.com/articles/chatgpt-agent-C-plus-plus/
声明:本文由 InfoQ 翻译,未经许可禁止转载。
吵翻了!到底该选 Rust 还是 Go,成2023年最大技术分歧
工信部要求所有 App、小程序备案;某国产电商被提名 Pwnie Awards “最差厂商奖”;阿里财报超预期 | Q资讯
微信扫码关注该文公众号作者