banner
Vinking

Vinking

你写下的每一个BUG 都是人类反抗被人工智能统治的一颗子弹

好玩愛玩!給文章頁接入大模型

前段時間,給文章接入了 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-8kmoonshot-v1-32kmoonshot-v1-128k 三個模型。
  • messages 陣列為對話的消息列表,其中 role 的取值為 systemuser 以及 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


載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。