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


加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。