跳到主要内容
2024-08-14
5 分钟
...
编程

文章摘要

这篇博文介绍了博主时隔一年后完全重写AI摘要后端的经历,起因是TianliGPT的摘要重复生成问题导致token消耗过快,于是博主开发了基于阿里云通义千问(qwen-long)、部署于Vercel并使用MySQL持久化存储的替代方案OrdChaosGPT,虽存在稳定性和速度上的不足,但已用于本站文章摘要生成,并分享了项目经历与技术实现细节。

时隔一年,终于抽出来时间完全重写了 AI 摘要的后端。

起因

TianliGPT 的文章摘要很好用,但是本来会能够根据 content 保证摘要不重复生成,现在不行了,不知道为什么。(我知道现在改为用 url 鉴别了,但是我的 js 最开始我就自己改过了,没有更新)

于是每刷新一次摘要就重新生成一次,token 余额哗哗掉。

想着与其重新改一遍 js,不如全权改为自己的版本,于是就有了这个项目。

现在本文(以及其它文章)开头的摘要就是来自 OrdChaosGPT 的了。

介绍

大概是 TianliGPT 的下位代替品

使用阿里云通义千问(qwen-long)作为摘要生成引擎,Vercel 部署,MySQL 数据库持久化数据储存

优势是无服务器(?真的是优势吗)

缺点是稳定性(至少我不提供 SLA 保证)与速度(依据文章长度与网速,获取到摘要的时间平均约 10 秒)

仓库地址:ordchaosgpt-cloud-function

经历

最开始还是想着用 rust 写一个服务端跑在 docker 上。

服务端倒是好写:

RUST
use actix_web::{post, web, App, HttpServer, Responder, HttpResponse, HttpRequest};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::fs;
use reqwest::Client;
use serde_json::json;

#[derive(Serialize, Deserialize)]
struct SummaryRequest {
    content: String,
    url: String,
}

#[derive(Serialize, Deserialize)]
struct SummaryResponse {
    url: String,
    summary: String,
}

struct AppState {
    auth_token: String,
}

const SUMMARY_DIR: &str = "summaries";

fn is_valid_referer(referer: Option<&str>) -> bool {
    if let Some(ref_url) = referer {
        let valid_domains = vec![
            ".ordchaos.com",
            ".ordchaos.top",
            ".ordchaos.eu.org",
        ];
        return valid_domains.iter().any(|domain| ref_url.ends_with(domain));
    }
    false
}

async fn generate_summary(content: &str) -> Result<String, Box<dyn std::error::Error>> {
    let api_key = "*******************************";
    let api_url = "https://dashscope.aliyuncs.com/compatible-mode/v1";
    let client = Client::new();

    let request_body = json!({
        "model": "qwen-long",
        "messages": [
            {"role": "system", "content": "You are a helpful summary generator."},
            {"role": "user", "content": format!("请为以下内容用中文生成长度为150汉字左右的摘要,摘要只有一个自然段,且只给出摘要即可,不要说其他任何话: {}", content)}
        ],
        "temperature": 0.8,
        "top_p": 0.8
    });

    let response = client
        .post(format!("{}/chat/completions", api_url))
        .header("Authorization", format!("Bearer {}", api_key))
        .json(&request_body)
        .send()
        .await?;

    let response_json: serde_json::Value = response.json().await?;
    let summary = response_json["choices"][0]["message"]["content"]
        .as_str()
        .unwrap_or("摘要生成失败:返回格式不正确")
        .to_string();

    Ok(summary.trim().to_string())
}

fn save_summary(abbrlink: &str, summary_data: &SummaryResponse) -> std::io::Result<()> {
    let summary_path = format!("{}/{}.json", SUMMARY_DIR, abbrlink);
    let json_data = serde_json::to_string(summary_data)?;
    fs::write(&summary_path, &json_data)?;
    Ok(())
}

fn load_summary(abbrlink: &str) -> Option<SummaryResponse> {
    let summary_path = format!("{}/{}.json", SUMMARY_DIR, abbrlink);
    if Path::new(&summary_path).exists() {
        if let Ok(json_data) = fs::read_to_string(&summary_path) {
            if let Ok(summary) = serde_json::from_str(&json_data) {
                return Some(summary);
            }
        }
    }
    None
}

#[post("/generate-summary")]
async fn generate_summary_handler(
    data: web::Data<AppState>,
    req: HttpRequest,
    summary_request: web::Json<SummaryRequest>
) -> impl Responder {
    if let Some(auth_header) = req.headers().get("Authorization") {
        let token = auth_header.to_str().unwrap_or("").trim_start_matches("Bearer ");
        if token != data.auth_token {
            return HttpResponse::Unauthorized().body("鉴权失败:Token不正确");
        }
    } else {
        return HttpResponse::Unauthorized().body("鉴权失败:缺失Token");
    }

    let referer = req.headers().get("Referer").and_then(|v| v.to_str().ok());
    let origin = req.headers().get("Origin").and_then(|v| v.to_str().ok());
    if !is_valid_referer(referer) && !is_valid_referer(origin) {
        return HttpResponse::Forbidden().body("拒绝生成摘要:来源不正确");
    }

    let abbrlink = summary_request.url
        .split('/')
        .filter(|&segment| !segment.is_empty())
        .last()
        .unwrap_or("");

    if abbrlink.is_empty() {
        return HttpResponse::BadRequest().body("Invalid URL format");
    }

    if let Some(existing_summary) = load_summary(abbrlink) {
        return HttpResponse::Ok().json(existing_summary);
    }

    let summary = match generate_summary(&summary_request.content).await {
        Ok(summary) => summary,
        Err(_) => return HttpResponse::InternalServerError().body("摘要生成失败"),
    };

    let summary_data = SummaryResponse {
        url: summary_request.url.clone(),
        summary: summary.clone(),
    };

    if let Err(err) = save_summary(abbrlink, &summary_data) {
        return HttpResponse::InternalServerError().body(format!("存储摘要失败: {}", err));
    }

    HttpResponse::Ok().json(summary_data)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    fs::create_dir_all(SUMMARY_DIR)?;
    let auth_token = std::env::var("AUTH_TOKEN").expect("AUTH_TOKEN 未被设置");

    println!("服务端已经在 http://localhost:11451 开启");

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(AppState {
                auth_token: auth_token.clone(),
            }))
            .service(generate_summary_handler)
    })
    .bind("127.0.0.1:11451")?
    .run()
    .await
}

(有兴趣就拿去用吧,标识原作者就好)

结果部署 docker 的时候服务器炸了,原因未知。

所以想着还是别再折腾我那辣鸡服务器了,遂转为用 JavaScript 写云函数,主打一个 Serverless.

没什么好说的,主要是第一次写不太熟练,本身还是挺简单的。

遇到的问题是 MongoDB 没连上,无奈改为 MySQL结果用的还是服务器上的数据库

部署

如果你想使用的话,前往这个项目的仓库参考readme.md即可。

部署非常简单,有手就行,要求自备 MySQL 数据库。

题外话

写了一上午的 JS(

那就这样,886

无服务器AI摘要后端——OrdChaosGPT
作者
序炁
发布于
2024-08-14
更新于
2024-08-14
许可协议
BY - 署名NC - 非商业性使用SA - 相同方式共享
QQ空间微博豆瓣XFacebookLinkedIn复制链接