前段时间,给文章接入了 TianliGPT ,实现了自动生成文章摘要(太长不看)模块。TianliGPT 提供了非常简单快捷的嵌入方式,但是 TianliGPT 专注于摘要生成以及相关文章推荐功能,如果想要对其进行拓展,则有比较大的局限性。所以最近放弃了 TianliGPT 转而使用 Moonshot AI 进行文章摘要的生成和额外功能的拓展。
确定需求#
首先,我们想除了给文章生成摘要以外,还能针对文章的内容提出相关问题以及对应的答案,点击问题后会显示对应问题的答案。效果类似于下图:
根据上面的需求,需要模型给我们返回类似于下面 JSON 格式的内容,再交给前端进行处理:
{
"summary": "文章总结内容",
"qa": [
{
"question": "问题 1",
"answer": "回答 1"
},{
"question": "问题 2",
"answer": "回答 2"
},
...
]
}
进而能设计出下面的 prompt 交给模型:
设计一个简明问题列表,目标是从文章中挖掘专业概念或未详尽之处。提供文章摘要,并针对特定概念形成 6 个问题。问题应精确,并生成相应答案。请使用如下 JSON 格式输出你的回复:
{
"summary": "文章总结内容",
"qa": [
{
"question": "问题 1",
"answer": "回答 1"
},
...
]
}
注意,请将文章总结内容放置在 summary 字段中,将问题放在 question 字段中,将回答放置在 answer 字段中。
Note
prompt(提示词)指的是给模型提供的信息或指令,用以引导模型生成特定的响应或输出,它决定了模型如何理解和处理用户输入。一个有效的 prompt 通常包括以下几个要素:明确性、相关性、简洁性、上下文、指令性。
由于 Kimi 和 Moonshot AI 提供的模型是同源的,所以我们可以通过使用 Kimi 测试,在一定程度上预测使用 Moonshot API 时可能获得的结果 (其实是为了省钱) 。为了确认这个 prompt 是否可以实现我们想要的效果,尝试使用它与 Kimi 对话,结果如下图:
与模型发起对话#
在解决了应该与模型交流什么这个问题之后,我们需要解决的是怎么与模型交流的问题。Moonshot AI 的官网文档提供了 Python 和 Node.js 两种版本的实现方法,而这里会使用 PHP 来实现相应的功能。
官方为我们提供了一个 Chat Completions 的 API:https://api.moonshot.cn/v1/chat/completions
,同时请求头和请求内容的示例如下:
# 请求头
{
"Content-Type": "application/json",
"Authorization": "Bearer $apiKey"
}
# 请求内容
{
"model": "moonshot-v1-8k",
"messages": [
{
"role": "system",
"content": "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。"
},
{ "role": "user", "content": "你好,我叫李雷,1+1 等于多少?" }
],
"temperature": 0.3
}
model
为模型名称,Moonshot-v1 目前有moonshot-v1-8k
、moonshot-v1-32k
、moonshot-v1-128k
三个模型。messages
数组为对话的消息列表,其中 role 的取值为system
、user
以及assistan
其一:system 代表系统消息,为对话提供上下文或指导,一般填入 prompt;user 代表用户的消息,即用户的提问或输入;assistant 代表模型的回复。temperature
为采样温度,推荐0.3
。
我们可以构建一个 MoonshotAPI
类来实现这个功能:
class MoonshotAPI {
private $apiKey;
private $baseUrl;
public function __construct($apiKey) {
$this->apiKey = $apiKey;
$this->baseUrl = "https://api.moonshot.cn/v1/chat/completions";
}
/**
* 发送并获取 API 响应数据
* @param string $model 模型名称
* @param array $messages 消息数组
* @param float $temperature 温度参数,影响响应的创造性
* @return mixed API 响应数据
*/
public function sendRequest($model, $messages, $temperature) {
$payload = $this->preparePayload($model, $messages, $temperature);
$response = $this->executeCurlRequest($payload);
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("无效响应格式。");
}
return $responseData;
}
/**
* 构造请求内容
* @param string $model 模型
* @param array $messages 对话的消息列表
* @param float $temperature 采样温度
* @return array 构造好的请求内容
*/
private function preparePayload($model, $messages, $temperature) {
return [
'model' => $model,
'messages' => $messages,
'temperature' => $temperature,
'response_format' => ["type" => "json_object"] # 启用 JSON Mode
];
}
/**
* 发送请求
* @param array $data 请求数据
* @return string API 响应数据
*/
private function executeCurlRequest($data) {
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $this->baseUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiKey
],
]);
$response = curl_exec($curl);
if ($response === false) {
$error = curl_error($curl);
curl_close($curl);
throw new RuntimeException($error);
}
curl_close($curl);
return $response;
}
}
Note
如果你直接在提示词 prompt 中告诉 Kimi 大模型:“请输出 JSON 格式的内容”,Kimi 大模型能理解你的诉求,也会按要求生成 JSON 文档,但生成的内容通常会有一些瑕疵,例如在 JSON 文档之外,Kimi 还会额外地输出其他文字内容对 JSON 文档进行解释。
所以这里我们需要在构造请求内容时启用 JSON Mode,让模型 “按照要求输出一个合法的、可被正确解析的 JSON 文档” ,即给 preparePayload
方法的返回数组内加上 'response_format' => ["type" => "json_object"]
。
显而易见,MoonshotAPI
类接受的三个参数中,只有 $messages
对话消息列表这个参数是比较复杂的,所以我们创建一个 getMessages
函数来构建对话消息列表数组。
/**
* 构建一个消息的数组
*
* @param string $articleText 文章内容
* @return array 包含系统和用户消息的数组
*/
function getMessages($articleText) {
return [
[
"role" => "system",
"content" => <<<EOT
设计一个简明问题列表,目标是从文章中挖掘专业概念或未详尽之处。提供文章摘要,并针对特定概念形成6个问题。问题应精确,并生成相应答案。请使用如下 JSON 格式输出你的回复:
{
"summary": "文章内容",
"qa": [
{
"question": "问题1",
"answer": "回答1"
},
...
]
}
注意,请将文章总结内容放置在 summary 字段中,将问题放在 question 字段中,将回答放置在 answer 字段中。
EOT
],
[
"role" => "user",
"content" => $articleText
]
];
}
这里我们把一开始设计好的 prompt 填入 system
消息,把文章内容填入第一个 user
消息中。在实际的 API 请求中,messages
数组将按会照时间顺序排列,通常首先是 system
消息,然后是 user
的提问,最后是 assistant
的回答。这种结构有助于维护对话的上下文和连贯性。
当我们与模型交流时,模型会返回这个样子的 JSON 数据,其中我们主要关注的是 choices
数组。
{
"id": "chatcmpl-xxxxxx",
"object": "chat.completion",
"created": xxxxxxxx,
"model": "moonshot-v1-8k",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "这里是模型的回复"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 229,
"completion_tokens": 64,
"total_tokens": 293
}
}
在我们这种对话模式中,choices
数组中通常只包含一个对象(这也就是为什么下面 Moonshot
函数获取模型回复等信息的时候写死 $result['choices'][0]
),这个对象代表模型生成的文本回复。对象内的 finish_reason
指示了生成文本的完成原因,如果模型认为已经给出了一个完整的回答,那么 finish_reason
的值将会为 stop
。所以我们可以据此来判断模型生成的内容是否完整。而对象内的 content
则为模型给我们的回复。
接着我们创建 Moonshot
函数来调用 MoonshotAPI
类:
/**
* 调用 MoonshotAPI 类
*
* @param string $articleText 文章内容
* @param string $model 使用的模型,默认为 "moonshot-v1-8k"
* @return array 返回带有状态码和数据的数组
*/
function Moonshot($articleText, $model = "moonshot-v1-8k") {
$apiKey = 'sk-xxxxxxxx'; # $apiKey 为用户中心申请的 API Key
$moonshotApi = new MoonshotAPI($apiKey);
$messages = getMessages($articleText);
$temperature = 0.3;
try {
$result = $moonshotApi->sendRequest($model, $messages, $temperature);
if (isset($result['error'])) {
throw new RuntimeException("模型返回的错误:" . $result['error']['message']);
}
# 判断模型生成的内容是否完整
$responseContent = $result['choices'][0]['message']['content'] ?? null;
if ($responseContent === null || $result['choices'][0]['finish_reason'] !== "stop") {
throw new RuntimeException("返回的内容不存在或被截断。");
}
# 因为我们启用了 JSON Mode 让模型给我们返回的回复是标准的 JSON 格式
# 所以需要过滤掉非标准 JSON 格式的回复
$decodedResponse = json_decode($responseContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("无效响应格式。");
}
return $result;
} catch (Exception $e) {
return ['stat' => 400, 'message' => $e->getMessage()];
}
}
至此,我们就得到了像下面的代码。如果没有意外,直接调用后将会得到模型的回复✌️。前端拿到回复后再渲染到页面即可,这里就不过多赘述。
header('Content-Type: application/json');
class MoonshotAPI {...}
function getMessages(...) {...}
function Moonshot(...) {...}
# 使用示例
try {
$article = "这是文章内容";
$aiResponse = Moonshot($article);
# 直接输出,或者对模型返回的结果进行后续处理
echo json_encode($aiResponse, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['stat' => 400, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
番外:超长文章的处理#
对于某些文章,直接调用上面代码可能会得到下面的错误:
{
"error": {
"type": "invalid_request_error",
"message": "Invalid request: Your request exceeded model token limit: 8192"
}
}
出现这种错误的原因是输入输出上下文的 token 总和超过了模型(这里我们默认使用的是 moonshot-v1-8k
模型)设定的 token 上限,我们需要根据上下文的长度,加上预期的输出 Tokens 长度来选择合适的模型。针对这种情况,文档也给出了 如何根据上下文长度选择恰当的模型 的示例代码,我们只需要将代码转为 PHP 然后接入我们上面的代码即可。
/**
* 估算给定消息的 token 数量。
* 使用文档给出的 estimate-token-count API 来估算输入消息的 token 数量。
*
* @param string $apiKey API 密钥
* @param array $inputMessages 要估算 token 数量的消息数组
* @return int 返回估算的 token 总数
*/
function estimateTokenCount($apiKey, $inputMessages) {
$header = [
'Authorization: Bearer ' . $apiKey,
];
$data = [
'model' => 'moonshot-v1-128k',
'messages' => $inputMessages,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://api.moonshot.cn/v1/tokenizers/estimate-token-count',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey
],
]);
$response = curl_exec($curl);
if ($response === false) {
$error = curl_error($curl);
curl_close($curl);
throw new RuntimeException($error);
}
curl_close($curl);
$result = json_decode($response, true);
return $result['data']['total_tokens'];
}
/**
* 根据估算的 token 数量选择最合适的模型。
*
* @param string $apiKey API 密钥
* @param array $inputMessages 消息数组
* @param int $defaultMaxTokens 默认的最大 token 数量限制
* @return string 返回选择的模型名称
*/
function selectModel($apiKey, $inputMessages, $defaultMaxTokens = 1024) {
$promptTokenCount = estimateTokenCount($apiKey, $inputMessages);
$totalAllowedTokens = $promptTokenCount + $defaultMaxTokens;
if ($totalAllowedTokens <= 8 * 1024) {
return "moonshot-v1-8k";
} elseif ($totalAllowedTokens <= 32 * 1024) {
return "moonshot-v1-32k";
} elseif ($totalAllowedTokens <= 128 * 1024) {
return "moonshot-v1-128k";
} else {
throw new Exception("Tokens 超出最大限制。");
}
}
Moonshot
函数下,当模型错误返回类型为 invalid_request_error
(即超出该模型最大 token 限制)时,调用 selectModel
函数选择最合适的模型后再重新用合适的模型进行对话。
function Moonshot($articleText, $model = "moonshot-v1-8k") {
...
if (isset($result['error'])) {
if ($result['error']['type'] === "invalid_request_error") {
$model = selectModel($apiKey, $messages);
return Moonshot($articleText, $model);
} else {
throw new RuntimeException("模型返回的错误:" . $result['error']['message']);
}
}
...
}
此文由 Mix Space 同步更新至 xLog
原始链接为 https://www.vinking.top/posts/codes/developing-auto-summary-module-using-ai