上周产品说文章列表的摘要别再截前30个字了,我花了一个下午接AI摘要API,中间踩了几个坑。今天把接入方案和踩过的坑一起摊开讲,你照着抄就能用。
这篇文章是我以云策API的AI摘要接口为例接入实践攻略。进入技术细节之前,有几件事得先交代。
平台背景:官网首页显示,平台成立于2022年,自称累计服务超过10000+开发者,日均调用量突破100万次。但运营主体是谁、有没有企业资质、后端对接的是哪个大模型——是自研还是套壳调用其他API——这些信息在官网和公开渠道都没找到,不确定。
SSL证书已过期:截至本文写作时,api.auth.top 的SSL证书已经过期,浏览器进行访问的时候会直接弹出安全警告,不手动跳过的话根本打不开官网。对于一个API服务平台来说,SSL证书过期不是小事——它直接影响数据传输的安全性。你决定接入之前,先确认证书是否已经续上。
隐私政策:隐私政策页面是存在的,不过内容却十分模板化,存在“我们可能收集以下类型的个人信息”以及“我们使用收集的信息用于”这类章节标题,而在标题下面几乎没有给出具体的说明。像你的文本数据传送过去之后会不会被存储、会不会被用于模型训练这些关键问题,都没有给出明确的回答。
计费模式:这个网站是完全免费的API网站,我已经注册使用,你可以自行去注册,然后自己在控制台查看就知道我说的是真是假。
以下方案的核心逻辑——校验、重试、超时、异步并发以及异步并发这些环节,并不依赖特定的平台特性,换成其他的AI摘要API同样可以正常使用。

我们网站的文章列表当中,摘要一直是粗暴截取正文的前30个字。直到有一天我自己去刷列表的时候,看到一篇深度测评的摘要显示的是“近年来随着智能手机市场的快速发展”。
这到底是什么呢?用户看了之后就好像没看一样。
我后来专门留意了一下,列表页用户平均停留还不到3秒,也就是在3秒之内就会决定点不点进去。要是给他一段毫无信息量的开头废话,那他凭什么会点进去呢?
也有不少人运用TextRank来开展抽取式摘要的工作,也就是从原文当中挑选出权重较高的句子再拼接起来。这样的做法听起来比较靠谱,但拼接出来的内容读起来就像是缝合怪,上下文会断成两截。这是因为它本质上就是一个“搬运工”,没办法重新组织语言,更没法把散落在不同段落当中的关键信息串联起来。
生成式摘要就不一样了。大模型会先读懂全文的语义,再用自己的话来概括一遍。整体逻辑通顺、信息密度高,同时噪音也比较少。云策API的AI摘要接口走的也就是这条路线。
核心信息:
- 接口地址:
https://api.auth.top/api/aizy - 请求方式:POST
- 认证方式:Header里传
Authorization: Bearer YOUR_API_KEY - 请求参数:就一个——
text - QPS上限:30次/秒
- 平均响应时间:约801ms
返回极简:
{
"code": 200,
"msg": "总结成功",
"data": "AI生成的摘要内容"
}
参数表就一个text——没有temperature、没有top_p、没有max_tokens。
这得两面看。好处是你不用研究参数怎么调,丢文本进去就能拿到摘要,接入速度很快。坏处是你没法控制摘要长度,也没法指定风格,比如”用三句话概括”或者”面向专业读者”。
要是你的业务只需要一段通顺的摘要,那这个极简设计刚好可以满足使用。要是你需要对输出进行精确控制,那么有两个思路可以选用:一是在应用层开展二次加工工作,比如借助规则把过长的摘要进行截断,或者再调用一次接口来对摘要做压缩处理;二是换用一个支持更多参数的接口。
直白来说,它适宜用于快速验证以及简单的场景当中,要是正式上线并且对摘要有着精细的要求,那就得自己补充一层逻辑。

去云策API官网注册,在控制台生成API Key。
拿到Key之后——别硬编码在代码里。万一代码上传到GitHub或者被误推到公开仓库,Key就裸奔了。
放到环境变量里,而且变量名别用太泛的 API_KEY,容易跟其他服务的Key冲突:
# Linux/macOS
export YUNCE_API_KEY="your_api_key_here"
# Windows PowerShell
$env:YUNCE_API_KEY="your_api_key_here"
我们可以借助 os.environ.get(“YUNCE_API_KEY”) 来完成代码里的读取操作。要是需要修改Key的话,只需要更改一个环境变量就可以,不用去改动代码然后重新部署。需要留意的是,修改环境变量之后,通常都要重启应用进程才能让它生效;如果是容器化部署的情况,那就需要重建容器,这样新的Key才会发挥作用。不过不管怎样,至少不用去改动代码,也不用走CI/CD流程。
我最开始写了一个只有5行的极简版本,顺利跑通了接口,但在上线之前发现缺少了校验、重试以及超时控制,裸写的代码根本就扛不住实际的运行压力。比如说有一次传输了一批文章,其中有一篇的内容是空的,接口返回了错误但代码没有去处理,直接抛出了异常,而调用方也没有进行try-catch,结果整个脚本直接停了。下面就是我迭代之后的版本,把该补上的内容都给补上了。
import os
import time
import random
import requests
class SyncAISummaryClient:
"""云策API AI摘要客户端 - 同步版"""
def __init__(self, api_key=None):
self.api_key = api_key or os.environ.get("YUNCE_API_KEY")
if not self.api_key:
raise ValueError("API Key没配置!设置环境变量YUNCE_API_KEY或传入api_key参数")
self.url = "https://api.auth.top/api/aizy"
self.max_retries = 3
self.timeout = 15 # 秒
def summarize(self, text, retry_count=0):
"""生成AI摘要,返回结构化结果"""
if not text or not isinstance(text, str):
return {"success": False, "error_type": "invalid_input", "message": "text不能为空,且必须是字符串"}
if len(text.strip()) < 10:
return {"success": False, "error_type": "invalid_input", "message": "文本太短了,少于10个字没法生成有意义的摘要"}
headers = {"Authorization": f"Bearer {self.api_key}"}
payload = {"text": text}
# 注意:日志中不要打印text内容,避免敏感信息泄露
try:
resp = requests.post(
self.url, headers=headers,
data=payload, timeout=self.timeout
)
resp.raise_for_status()
result = resp.json()
if result.get("code") == 200:
return {"success": True, "data": result["data"]}
else:
return {
"success": False,
"error_type": "api_error",
"message": f"API返回错误: {result.get('msg', '未知错误')}"
}
except requests.exceptions.Timeout:
if retry_count < self.max_retries:
# 指数退避 + 随机抖动,避免多客户端同时重试形成惊群效应
wait = (2 ** retry_count) + random.uniform(0, 1)
print(f"请求超时,{wait:.1f}秒后第{retry_count+1}次重试...")
time.sleep(wait)
return self.summarize(text, retry_count + 1)
return {
"success": False,
"error_type": "timeout",
"message": "请求多次超时,请检查网络或稍后重试"
}
except requests.exceptions.RequestException as e:
return {
"success": False,
"error_type": "network_error",
"message": f"网络请求失败: {str(e)}"
}
使用:
client = SyncAISummaryClient()
result = client.summarize("你的长文本内容...")
if result["success"]:
print(f"摘要: {result['data']}")
else:
print(f"出错了[{result['error_type']}]: {result['message']}")
几个设计决策解释一下。
指数退避加随机抖动——超时后不立刻重试,1秒、2秒、4秒逐步拉长间隔。加上 random.uniform(0, 1) 的抖动,是因为如果你部署了多个实例同时超时、同时重试,不加抖动的话它们会在同一时刻涌入,形成惊群效应。这个小细节在生产环境里差别很大。
超时15秒——接口平均响应800ms,但长文本会慢。我最初设了5秒,结果超500字几乎必超时,调到15秒才稳。
返回结构化结果——统一用 {"success": True/False, "error_type": ..., "message": ...} 的格式,不管参数校验失败、API报错、超时、网络异常,调用方都用同一套逻辑处理:检查 success,根据 error_type 做针对性处理。超时可以重试,API错误可以告警,网络错误可以走降级逻辑。比抛异常让调用方到处try-catch靠谱得多。

官方文档提到了两种认证的方式。上面所使用的是通过Header来传递Bearer Token的方式,另一种则是把密钥放到请求体当中的方式:
payload = {"text": "你的文本", "key": "YOUR_API_KEY"}
response = requests.post("https://api.auth.top/api/aizy", data=payload)
这两种方式都可以使用,但我强烈推荐采用Header的方式。把Key跟业务数据分开之后,代码会变得更加清晰,日志脱敏的操作也会更为方便。要是把Key混在Body当中,哪天在打印日志的时候不小心把整个请求体都打出来了……那就会变得十分尴尬。我之前有个同事把Key写在了配置文件里,而配置文件又被提交到了私有Git仓库,后来仓库被误设为了public,那场面实在是让人不忍回忆。
实际业务当中你往往不是去处理一篇文章,而是几百上千篇。同步版本一个一个去跑速度太慢,那就得上异步的方式。
但需要注意,QPS上限是30次/秒。你不能毫无顾忌地开启100个并发往里灌。10个并发是个保守的安全值——以800ms平均响应时间来计算,10并发的理论吞吐约12.5 QPS,但实际网络存在波动、长文本的响应可能从800ms拉长到几秒,10并发留出了足够的余量。我以10并发跑了约500篇文章,没有触发过限流。
异步版同样需要重试机制,不能比同步版矮一截:
import os
import asyncio
import random
import json
import aiohttp
class AsyncAISummaryClient:
"""云策API AI摘要客户端 - 异步版"""
def __init__(self, api_key=None, max_concurrent=10, max_retries=3):
self.api_key = api_key or os.environ.get("YUNCE_API_KEY")
if not self.api_key:
raise ValueError("API Key没配置!设置环境变量YUNCE_API_KEY")
self.url = "https://api.auth.top/api/aizy"
self.max_concurrent = max_concurrent
self.max_retries = max_retries
self.timeout_seconds = 15
async def summarize_one(self, session, text, sem, retry_count=0):
"""单次异步调用,带重试,返回结构化结果"""
if not text or len(text.strip()) < 10:
return {"success": False, "error_type": "invalid_input", "message": "文本过短"}
async with sem:
try:
async with session.post(
self.url,
headers={"Authorization": f"Bearer {self.api_key}"},
data={"text": text}
) as resp:
resp.raise_for_status()
# HTTP状态异常走 ClientError 分支,业务错误码走 api_error 分支
result = await resp.json()
if result.get("code") == 200:
return {"success": True, "data": result["data"]}
return {
"success": False,
"error_type": "api_error",
"message": result.get("msg", "未知错误")
}
except asyncio.TimeoutError:
# ClientTimeout(total=15) 触发的整体超时,走这里
# 如果你后续加了 sock_read 或 sock_connect 超时,
# 需要额外捕获 aiohttp.ServerTimeoutError(它是 ClientError 的子类)
if retry_count < self.max_retries:
wait = (2 ** retry_count) + random.uniform(0, 1)
await asyncio.sleep(wait)
# 当前假设超时是服务端波动导致,复用同一session重试
# 如果遇到 aiohttp.ClientConnectionError 或连接池相关错误,
# 建议在调用层重建session之后再重试
return await self.summarize_one(session, text, sem, retry_count + 1)
return {
"success": False,
"error_type": "timeout",
"message": "多次超时"
}
except (aiohttp.ContentTypeError, json.JSONDecodeError):
# 上游返回了非JSON响应(比如nginx的502 HTML页面),不是网络错误
return {
"success": False,
"error_type": "parse_error",
"message": "上游返回了非JSON响应"
}
except aiohttp.ClientError as e:
# 网络层错误:连接断开、DNS解析失败、HTTP状态异常等
return {
"success": False,
"error_type": "network_error",
"message": str(e)
}
except Exception as e:
return {
"success": False,
"error_type": "unknown",
"message": str(e)
}
async def batch(self, texts):
"""批量生成摘要"""
sem = asyncio.Semaphore(self.max_concurrent)
timeout = aiohttp.ClientTimeout(total=self.timeout_seconds)
async with aiohttp.ClientSession(timeout=timeout) as session:
tasks = [self.summarize_one(session, t, sem) for t in texts]
return await asyncio.gather(*tasks)
# 使用
async def main():
client = AsyncAISummaryClient(max_concurrent=10)
articles = ["文章1内容...", "文章2内容...", "文章3内容..."]
results = await client.batch(articles)
for i, r in enumerate(results):
if r["success"]:
print(f"文章{i+1}摘要: {r['data']}")
else:
print(f"文章{i+1}失败[{r['error_type']}]: {r['message']}")
asyncio.run(main())
跟同步版对齐了:指数退避加随机抖动、结构化返回、参数校验、raise_for_status。异常处理分了四层——asyncio.TimeoutError 是 ClientTimeout(total=15) 触发的整体超时,走重试逻辑;aiohttp.ContentTypeError 和 json.JSONDecodeError 是上游返回了非JSON响应(比如nginx的HTML错误页),归类为 parse_error 而非网络错误,调用方不会误判成自己网络有问题;aiohttp.ClientError 覆盖网络层错误和HTTP状态异常,直接返回;其他未预期的错误单独兜底。
关于重试复用session的问题:当前假设超时是由服务端波动所导致的,复用同一session来进行重试一般不会有问题。
但如果遇到的是aiohttp.ClientConnectionError或是连接池耗尽这类session层面的问题,重试用同一个session可能会继续失败。这种情况建议在调用层重建session之后再进行重试,代码里为了简洁起见没有展开相关内容,生产环境可以根据实际需求进行补充。

实际场景当中,你的后端会去充当中间层,前端不会直接去调用云策API。这样一来Key就不会暴露出来,同时还能够在后端开展缓存以及限流的相关工作。
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
import os
import json
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
class SummaryRequest(BaseModel):
text: str
@app.post("/api/summary")
async def generate_summary(req: SummaryRequest):
if len(req.text.strip()) < 10:
raise HTTPException(status_code=400, detail="文本内容过短,至少需要10个字")
api_key = os.environ.get("YUNCE_API_KEY")
if not api_key:
raise HTTPException(status_code=500, detail="服务配置错误")
try:
async with httpx.AsyncClient(timeout=15.0) as client:
resp = await client.post(
"https://api.auth.top/api/aizy",
headers={"Authorization": f"Bearer {api_key}"},
data={"text": req.text}
)
resp.raise_for_status()
try:
result = resp.json()
except (json.JSONDecodeError, httpx.DecodingError):
logger.error(f"上游返回非JSON响应, status={resp.status_code}, body={resp.text[:200]}")
raise HTTPException(status_code=502, detail="上游AI服务返回了非预期响应")
if result.get("code") == 200:
return {"success": True, "summary": result["data"]}
else:
raise HTTPException(
status_code=400,
detail=f"AI处理失败: {result.get('msg')}"
)
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="AI服务响应超时,请稍后重试")
except httpx.HTTPStatusError as e:
logger.warning(f"上游AI服务返回异常状态: {e.response.status_code}")
raise HTTPException(status_code=502, detail="上游AI服务异常")
except httpx.RequestError as e:
logger.error(f"无法连接AI服务: {str(e)}")
raise HTTPException(status_code=502, detail="无法连接AI服务")
except HTTPException:
raise
except Exception:
logger.exception("生成摘要时发生未预期错误")
raise HTTPException(status_code=500, detail="服务内部错误")
前端调你自己的 /api/summary 就行,Key永远在后端。
异常处理分了多层:JSON解析失败(上游可能返回了HTML错误页)、超时(504)、上游HTTP状态异常(502)、网络连接错误(502)、未预期错误(500+完整日志)。这样出问题你扫一眼日志就知道是云策API挂了、返回了乱七八糟的东西、还是你代码有bug。
这里有一个容易踩的坑:except HTTPException: raise 这行不能省。FastAPI的HTTPException是Exception的子类,如果不单独re-raise,它会被最后的兜底捕获,所有业务异常都变成500。
选FastAPI而不是Flask,是因为它原生支持async,跟httpx搭配顺手。你团队如果已经用Flask了,也没必要为这一个接口换框架。
坑1:文本太长会超时
我试过传一篇8000字的文章,等了14秒才返回。超过1万字直接超时了。单次调用控制在5000字以内比较稳妥。超过的话,先分段再合并——每段生成摘要,最后把各段摘要再喂给接口做一次总摘要。
坑2:摘要质量跟文本类型强相关
新闻、论文这类逻辑清晰的文本,摘要效果很好。但意识流小说、大量对话的剧本,AI也会犯懵。这不是接口的问题,是生成式摘要本身的局限。对于文学性文本,我建议还是人工写摘要。
坑3:别拿它当实时接口用
800ms的响应时间,做后台批处理绰绰有余。但如果你要在用户请求的同步链路里实时生成摘要——用户点进去白屏等一秒,体验很差。正确做法是预生成:文章发布时就把摘要存好,用户看的时候直接读数据库。
坑4:Key泄露了怎么办?
三步走:
- 立刻重生成Key——去控制台重新生成,旧Key会立即失效,堵住漏洞。
- 检查泄露期间的调用日志——看控制台里有没有异常消耗,确认有没有人用你的Key恶意调用。
- 排查泄露原因——是代码仓库公开了?还是日志把请求体打出去了?找到根因才能防止再次发生。
这也是为什么我一直强调Key要放在环境变量当中、不要进行硬编码——真要是出了状况,只需要修改一个环境变量就可以了(修改完成之后记得重启进程,要是采用容器化部署的话则需要重建容器,新的Key才会生效),不需要去修改代码然后重新进行部署。
要是你的产品存在内容展示的场景,比如文章列表、资讯流、知识库当中,那么接入AI摘要大概率是具备价值的。精准的摘要和截取前30个字相比,在用户体验上是质的区别。至于点击率提升多少,我没做过A/B测试,不敢给出具体数字,但逻辑上更清晰的摘要一定比废话截断更容易吸引点击。
SEO方面也会带来正面的影响。生成式摘要天然就可以做到语义通顺,并且包含相关的关键词,把它塞进meta description当中的时候,搜索引擎在理解页面主题时会比截取式摘要更加准确——尤其是现在Google和百度越来越重视语义相关性,而不仅仅是依靠关键词匹配,一段语义完整的摘要比“近年来随着……”这种截断文本对搜索排名更加友好。
技术成本呢?一个POST请求,半天能上线。QPS 30次/秒对中小项目够用。
但有一点需要提醒:这个平台的SSL证书曾经出现过过期的情况,同时隐私政策也不够透明,在接入之前务必先确认好证书的状态以及数据处理政策。要是你的业务确实需要摘要功能,这里面的代码直接拿去进行修改就可以使用,核心逻辑更换成别的AI摘要API也同样适用。在适配你自己的场景的时候,注意调整一下并发数以及超时阈值就可以了。

好了,代码拿走,那些可能出现的坑我已经替你提前踩过了。








暂无评论内容