前段時間,給文章接入了 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