大模型的出现带来了新的技术革新,它强大的对话,分析,生成能力可以应用在音乐的很多方面。我们希望借助大模型的能力, 实现对站内外音乐热点词条内容进行抽取,分析,总结,推荐。 本文将系统的介绍我们如何基于RAG 搭建一个带前端页面的 热点AI检索功能agent
体验地址:http://llm-zq.jupyter.panshi-gy.netease.com/

1.背景

大模型的出现带来了新的技术革新,它强大的对话,分析,生成能力可以应用在音乐的很多方面。我们希望借助大模型的能力, 实现对站内外音乐热点词条内容进行抽取,分析,总结,推荐。 但是:

  • 大模型对于时事热点等,幻觉能力严重,而RAG(检索增强生成)可以解决这个问题。
  • 很多都离不开外部的依赖接口,无法做到完全的offline, 且当token量大之后,费用也很大, 但其实开源的很多模型如LLAMA, QWEN等都已经有非常不错的能力。而且近期流行的ollama框架, 也让个人PC也都能支持大模型生成。
  • 我们希望借助开源的能力,来快速搭建一个不依赖外部接口的AI检索引擎来为我们服务, 也避免了隐私泄露的风险。

它的主要特点:

  • 不依赖外部接口, 离线实现LLM生成, 检索,embedding等能力。
  • 基于互联网结果进行RAG,解决模型生成幻觉的问题,尤其可以支持对于近期热点知识的总结。
    本文主要介绍开发这个agent的框架,一些技术细节和思路,希望给大家带来一点LLM 开发的收获。效果图如下,左边是我们的agent, 输入问题描述,系统即可自动调用搜索引擎并爬取互联网的内容,并通过大模型分析总结返回给我们问题的结果。在某些情况下,甚至比KIMI的效果还要好。

img.png

2.框架

总体框架如下图所示,主要包括3个子模块:

  • (1) 检索爬取服务:根据用户搜索的热点关键词,调用自建的searxng 匿名检索服务系统, 获取top的互联网搜索引擎结果,并爬取相关网址全文内容。
  • (2) 文档召回服务:对爬取的全文内容切块,进行向量化,同时对query也进行向量化,计算query和文档的相关性,并进行排序选取top的文档切块
  • (3) 大模型生成服务。离线部署好大模型,输入相关文档和配置的prompt, 生成相关的检索答案汇总,并通过部署的streamlit前端服务返回给用户。

img_1.png

3个模块通过langchain框架进行串联起来工作,api接口都采用fastapi进行封装, 前端展示用streamlit进行交互开发。

3. 实现

基于基本的框架思路,我们前期调研了发现github已有类似的相关项目,在这些项目的基础上,我们做了一些优化。

LLocalSarch:https://github.com/nilsherzig/LLocalSearch

LangChain-SearXNG: https://github.com/ptonlix/LangChain-SearXNG

3.1 检索爬取服务

检索爬取服务主要有两个模块。searxng检索服务 和爬虫服务

3.1.1 searxng检索服务

SearXNG 是一个免费的互联网元搜索引擎,它聚合了来自各种搜索服务(如 google, duckduckgo等)和数据库(如wiki)的结果,但摆脱了隐私追踪。

当然,你也可以采用商业的搜索api 接口,比如google的Serper API , bing的Bing Web Search API,但这不是我们的目的,我们是希望搭建一个完全没有外部依赖的检索服务。

请注意,搭建searxng检索需要一台非大陆的VPS,并配有ipv4地址,如果嫌麻烦,可以用公共的searxng, 但是会有限制,地址:https://searx.space(需要FQ)

img_2.png

以下是搭建教程:

  1. 第一步:安装docker, docker-copose

docker安装:https://yeasy.gitbook.io/docker_practice/install/debian

docker-copose安装:https://yeasy.gitbook.io/docker_practice/compose/install

  1. 第二步:拉取searxng 镜像, 修改配置

修改项目docker配置

1
2
3
4
5
# 拉取代码
git clone https://github.com/searxng/searxng-docker.git
# docker配置里包括3个服务,caddy 做反向代理,redis存储数据,searxng主服务
#不做反向代理可以注释掉caddy部分, 只需要修改 searxng里的port,如: 0.0.0.0:8180:8080, 右边是设置好的容器内的端口,左边是本地端口可以改
vim searxng-docker/docker-compose.yaml

img_3.png

img_4.png

修改searxng主服务配置

1
2
3
sed -i "s|ultrasecretkey|$(openssl rand -hex 32)|g" searxng-docker/searxng/settings.yml # 生成一个密钥
# limiter: 改为false, 为true会限制你的请求频率,公开服务会开启,但是私人搭建的可以关闭
vim searxng-docker/searxng/setting.yml

img_5.png
3.第三步:启动compose 服务组

1
2
cd searxng-docker
docker-compose up -d
  1. 第四步:关闭端口防火墙并验证,如果没有防火墙则不需要这一步
1
ufw allow 8180

最后浏览器打开ip:8180,即可看到自己搭建的searxng页面并进行检索了,是不是很酷😎,没有任何广告,页面非常干净。

img_6.png

3.1.2 爬虫服务

单独searxng的结果信息量比较小,而对于LLM来说,丰富的信息意味着更准确的结果。 所以针对搜索引擎给出的相关网页,我们可以采用爬虫爬取top网页结果。 所幸,langchain(一个帮助在应用程序中使用大型语言模型的编程框架) 里就包含了相应的网页爬取模块,和文本解析模块。

1
2
3
4
# langchain 调用searxng示例, 获取top结果
from langchain_community.utilities import SearxSearchWrapper
s = SearxSearchWrapper(searx_host="http://localhost:8180")
s.run("what is a large language model?")
1
2
3
4
5
6
7
8
9
# langchain 爬取示例
from langchain_community.document_loaders import AsyncChromiumLoader
from langchain_community.document_transformers import Html2TextTransformer
urls = ["https://www.baidu.com"]
loader = AsyncChromiumLoader(urls, user_agent="MyAppUserAgent")
docs = loader.load() # 爬取
html2text = Html2TextTransformer()
docs_transformed = html2text.transform_documents(docs) # 解析抽取网页里文本
docs_transformed[0].page_content[0:500]

这里面在实践中存在几个主要问题:

  1. searxng的top结果中可能存在无法访问的(大陆),比如wiki 等,需要额外处理过滤。 这里我采用的是pac方式。过滤不能访问的网址
1
2
3
4
5
6
7
8
9
10
11
12
# wget https://raw.githubusercontent.com/petronny/gfwlist2pac/master/gfwlist.pac
import pacparser
pacparser.init()
pacparser.parse_pac('gfwlist.pac')

def is_direct(url):
ret = pacparser.find_proxy(url)
return "DIRECT" == ret

if __name__ == "__main__":
print(is_direct("www.baidu.com"))
print(is_direct("www.google.com"))
  1. 可能存在超时的问题,有些网站链接速度非常慢,原本的langchain 爬取模块不支持超时,需要自己在外面额外封装一层超时控制。或者采用httpx的包进行批量爬取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import httpx
from typing import List, Optional,Tuple
import asyncio
headers = {'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'}


async def get_result(url: str):
if not is_direct(url): # 非直连
async with httpx.AsyncClient(proxy='socks5://127.0.0.1:1080') as client:
try:
response = await client.get(url,headers=headers,timeout=10.0) # 设置超时
return url, response
except httpx.RequestError:
return url, None
else:
async with httpx.AsyncClient() as client:
try:
response = await client.get(url,headers=headers,timeout=10.0)
return url, response
except httpx.RequestError:
return url, None

async def get_results( urls: List[str]):
tasks = [get_result(url) for url in urls]
results = await asyncio.gather(*tasks)
for url, response in results:
if response is None:
print(f"URL: {url} - Failed to connect")
# else:
# print(url, response.text[:100])
return results

def get_results_access( urls: List[str]) -> List[Tuple[str,str]]:
try:
asyncio.get_running_loop()
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(asyncio.run, check_urls(urls))
results = future.result()
except RuntimeError:
results = asyncio.run(check_urls(urls))

return [(url,response.text) for url, response in results if response is not None]

  1. 爬取的结果如果是动态加载的内容,目前无法爬取。 比如 B站视频下的评论, 知乎的答案等。这种需要针对特定网站, 用自动化测试工具,比如Selenium 或者playwright. 这个待后续优化。

3.2 切块召回服务

这一步,其实主要对应RAG里R即retrieval, 召回。因为获取的top网址文本内容量比较大,一般单个网页的文本都接近5k token, 像百度知道这种以文本内容为主的基本都超过8k长度,多个网页内容直接丢给大模型解析,是个不太现实的任务,虽然现在有学者提出超长上下文的大模型(Long Context LLM)正在慢慢取代RAG, 但目前来说rag还是最优解。

召回过程是分为 切块,向量化,排序

3.2.1 切块

所有的文档进行chunk, 即切块, 比如以512个 token 作为一个chunk。这里面有几个问题:

  1. 如何确定最佳块大小?

这个目前没有定论,主要还是取决于应用场景,具体可以参考微软[1]的建议并自行进行测试:

img_7.png

  1. 分割策略?

为了得到更好的结果,我们可以重叠相邻的块。来自微软分析的分块策略比较,显示512 tokens分块和25%的重叠是比较好的分块策略。 当然也要考虑embedding的模型

img_8.png

实际使用下来,应用于网页文本分块召回的比较好的参数, chunk=500,overlap=100, 向量模型采用BCE。

3.2.2 向量化

切块之后第二步就是对文档和query都进行向量化,并计算 query和 文档之间的相似度,再设定过滤的阈值,得到最终我们需要的文档片段。那么,向量模型该如何选取?

一般的商业大模型服务都自带embedding接口,比如openai的 v1/embedding, 这种需要api_key, 显然不是我们的目标。开源模型效果对比,可以参考,huggingface 的embedding竞技场:https://huggingface.co/spaces/mteb/leaderboard ,但是里面不是所有模型都有打分,下面是一些主流的embedding模型:

img_9.png

开源模型挑选可以从几个方面入手:

① 硬件性能。 因为单次用户请求,涉及很多切块文档,所以需要考虑机器性能和模型速度,其实很多常见的大模型做embedding效果也很好,但它不是主流因为效率很低,我们在mteb 评测榜单上可以看到 qwen2的检索效果非常好,但是模型太大很难应用。 尤其我们的任务都是实时算,并不存储向量,所以需要模型不太大。

img_10.png

② 向量维度。向量维度会影响到 存储以及检索耗时,对于常见的检索任务,是对知识库的内容预先算好相应的向量,并存储进向量数据库。 用户检索时,对检索词向量化,再通过近邻检索算法检索最相关的top结果。当数据量显著大时,向量维度越大,检索耗时越明显。我们的任务里不存储向量,所以这块也不需要考虑。

③ 最大输入长度。 指模型处理输入的最大token长度,这个和我们前面提到的分块大小息息相关,因为如果分块大小超过最大长度,则超过的部分会被向量模型丢弃,导致信息损失。

④ 支持语言。大部分开源向量模型只支持单一或者有限的文本语言,在需要多语言需求的场景可能不合适。需要注意的是,不支持多语言,不代表其他语言就不能向量化,而是缺乏跨语言匹配的能力。 比如[ ‘How is the weather today?’, ‘今天天气怎么样?’] 在单一语言里相似度可能很低,而对于多语言,则匹配度较高。一般来说,如果只是针对特定语言,选择单一语言模型即可,评分高的混合语言模型不一定比单一语言模型效果好。 由于网页内容繁杂,我们倾向于选择多语言模型

⑤ 领域表现。通用 Embedding 模型在特定垂直领域(如医学、法律和金融等)可能不如专用模型有效。这些领域通常需要专门训练 Embedding 模型来捕捉特定的专业术语和语境。为特定业务需求优化的 Embedding 模型能够显著提升检索和生成的质量。 网页内容匹配通常不需要考虑领域表现。

基于上面的维度,我们选择了中英双语的 bce-embedding-base_v1模型。

3.2.3 排序

顺便再聊一下,关于RAG中的召回,目前主流的做法是两个阶段。第一阶段query和文档向量化,检索框架采用faiss, 或者milvus 这种向量查询数据库。 第一阶段存在两个问题:

1、当doc数据量大的时候,检索算法都是近似的, 不是挨个遍历计算,会有损。除非用暴力挨个计算cos, 但这个不现实。(在本任务里是可以的,因为文档量很小)

2、embedding本来就是对于信息的压缩,对原始文本信息是有丢失的。

那么对于这些缺点,有办法优化吗? 答案是有的,即第二阶段rerank模型精排。 rerank模型输入query和doc对文本,而不是emebdding, 信息无损。 2阶段检索详情可以参考QAnything给出的示意图, 很清楚。

img_11.png

在加入二阶段rerank之后,BCE的效果, top10命中率由85.91%提升到93.46%,非常明显。同时可以看到,采用hybird, 即bm25和embedding召回,再经过rerank可以达到最好的效果96.36%。

img_13.png
以下是有道 给出的BCE最佳实践

最佳实践(Best practice) :embedding召回top50-100片段,reranker对这50-100片段精排,最后取top5-10片段。

BAAI(北京智源人工智能研究院)也给出了BGE的最佳实践:

For multilingual, utilize BAAI/bge-reranker-v2-m3 and BAAI/bge-reranker-v2-gemma
For Chinese or English, utilize BAAI/bge-reranker-v2-m3 and BAAI/bge-reranker-v2-minicpm-layerwise.
For efficiency, utilize BAAI/bge-reranker-v2-m3 and the low layer of BAAI/ge-reranker-v2-minicpm-layerwise.
For better performance, recommand BAAI/bge-reranker-v2-minicpm-layerwise and BAAI/bge-reranker-v2-gemma

其实我们很容易联想两阶段召回, 其实就是早期的类 DSSM 双塔召回的不同思路。

  • 第一阶段,就是取双塔的最后一层向量做 近邻检索

  • 第二阶段,就是双塔放入query和doc计算的最后的打分

如果想要在自己领域内有更好的效果,也可以选择在领域数据集上微调模型。微调数据如下所示,正样本和负样本,并通过一些hard negative 的方式做样本增强。 现在也有一些思路是用LLM 来对原样本进行一些改写增强,比如给问题换个说法,比如“什么是深度学习?” -> “怎么理解深度学习?”, 这样都能提高原模型在特定领域的效果。

1
2
{"query": "如何提高机器学习模型的准确性?", "pos": ["通过交叉验证和调参可以提高模型准确性。"], "neg": ["机器学习是人工智能的一个分支。"]}
{"query": "什么是深度学习?", "pos": ["深度学习是机器学习的一个子领域,涉及多层神经网络。"], "neg": ["数据科学是一门交叉学科。"]}

3.3 大模型生成服务

这一步,主要是利用大模型的分析和总结能力,对检索到的相关文档和用户query进行分析,给出用户想要的结果。这里的核心问题也包括几块,1、大模型的选择。 2、prompt调优 3、服务部署以及前端展示 4. inference加速

3.3.1 大模型选择

市面上的开源大模型非常多,其中比较流行的有meta的 llama系列,最新是llama3, 以及Mistral(large不开源) ,google的Gemma(large不开源), 国内的 智普的chatglm,最新是chatglm4, 阿里的qwen,最新是qwen2, 以及baichuan等等非常多。那么这么多开源大模型,如何挑选适合我们的大模型:

  • 模型参数量,适配显存。第一维度需要考虑的就是机器的GPU显存,以下表格,以llama为列子一些常见的模型显存占用,显存占用主要分为2块,
  • 一块是加载模型参数占用的显存,在fp16精度下,1B约等于2G显存,可以按这个换算;
  • 另一块是生成时,计算的临时变量,以及kvcache占用的显存。在fp16精度下, 1K长度约等于1G, 两者加起来才是跑大模型时的最大显存占用。

img_14.png

  • 模型效果。可以参考一些大模型评测网站,比如:https://www.datalearner.com/ai-models/leaderboard/datalearner-llm-leaderboard, 选排在前面的基本没错。不过也需要针对自己的任务多试一些对比。
  • 任务适配度。不同的模型训练的领域是不太一样的,比如说,有的在数学相关数据集上训练的多,那么它可能在数学,推理方面效果很好,有些模型是为了做coding的, 有些是做图文的,选择的模型需要适配你自己的任务。如果只是想要简单聊天,那综合性能好的即可。对于这个专门的阅读文档总结用户问题,并需要遵循一定指令的任务,最好选用指令微调的模型

img_15.png

  • 社区成熟度。开源模型的一个重要力量,成熟社区模型能让各个框架迅速支持,可用的轮子很多,这也是我们选用的一个重要参考。

基于以上选择思路,我们选择了LLAMA3-8B-instruct 作为大模型来应用,LLAMA3主要是在英文语料上训练的,要想在中文上有比较好的效果,可以继续预训练,网上也已经有很多预训练好的中文LLAMA3, 我们选取的是hfl/llama-3-chinese-8b-instruct-v3

3.3.2 prompt调优

选定大模型之后,就是如何使用的问题了,大模型的角色,包含[‘system’, ‘user’, ‘assistant’]

system 一般代表整个大模型服务。指导模型如何输出,prompt一般放在这里
user 指代的是用户的输入,包括文本,语音,视频等等的输入数据
assistant 代表大模型的相应输出

在我们这个任务中,我们希望大模型根据 我们提供的数据,来对网页内容进行分析,所以我们的prompt

1
2
3
4
5
6
7
您是一位专业的研究员和作家,负责回答任何问题。
基于提供的搜索结果,为给定的问题生成一个全面而且信息丰富、但简洁的答案,长度不超过 500 字。您必须只使用来自提供的搜索结果的信息。使用公正和新闻性的语气。将搜索结果合并成一个连贯的答案。不要重复文本。
如果上下文中没有与当前问题相关的信息,只需说“嗯,我不确定。”不要试图编造答案。
位于以下context HTML 块之间的任何内容都是从知识库中检索到的,而不是与用户的对话的一部分。
<context>
{context}
<context/>
  • 设定角色: 开始给模型设定好角色, 研究员和作家
  • 指示: 无二义性的任务描述,基于搜索结果总结一个用户问题答案,非口语化,500字,不重复,没结果时也不能乱说
  • 上下文:使用明确的xml格式定义好输入的搜索结果

可以多给LLM一些例子看返回结果,根据返回结果对prompt做一定调整。

3.3.3 服务部署以及前端展示

选定模型之后要部署相应的后端模型服务和前端用户交互服务。

后端:

  • 提供模型对话服务给前端进行交互,这里最经典就是openai的 api接口sdk, 为了整个系统的兼容性,我们可以将我们的服务端部署成OPENAI API接口的形式

  • 我们选取的是python目前比较流行的FastAPI, FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架

  • 实现接口主要包括两个,1个是LLM对话服务(v1/chat/completions), 1个是query的embedding服务(v1/embeddings)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@app.post("/v1/chat/completions", response_model=ChatCompletionResponse)
async def create_chat_completion(request: ChatCompletionRequest):
global model, tokenizer

if len(request.messages) < 1 or request.messages[-1].role == "assistant":
raise HTTPException(status_code=400, detail="Invalid request")

gen_params = dict(
messages=request.messages,
temperature=request.temperature,
top_p=request.top_p,
max_tokens=request.max_tokens or 1024,
echo=False,
stream=request.stream,
repetition_penalty=request.repetition_penalty,
tools=request.tools,
)
logger.debug(f"==== request ====\n{gen_params}")
for each_message in request.messages:
info = str(each_message.role) +"\:" +str(len(each_message.content))
logger.debug(f"==== ===== ===")
logger.debug(f"==== message len ====\n{info}")
logger.debug(f"==== ===== ===")

# Here is the handling of stream = False
response = generate_llama3(model, tokenizer, gen_params)

# Remove the first newline character
if response["text"].startswith("\n"):
response["text"] = response["text"][1:]
response["text"] = response["text"].strip()

usage = UsageInfo()
message = ChatMessage(
role="assistant",
content=response["text"],
function_call= None,
)

logger.debug(f"==== message ====\n{message}")

choice_data = ChatCompletionResponseChoice(
index=0,
message=message,
finish_reason="stop"
)
task_usage = UsageInfo.model_validate(response["usage"])
for usage_key, usage_value in task_usage.model_dump().items():
setattr(usage, usage_key, getattr(usage, usage_key) + usage_value)

return ChatCompletionResponse(
model=request.model,
id="", # for open_source model, id is empty
choices=[choice_data],
object="chat.completion",
usage=usage
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@app.post("/v1/embeddings", response_model=EmbeddingResponse)
async def get_embeddings(request: EmbeddingRequest):

embeddings = [embedding_model.encode(text) for text in request.input]
embeddings = [embedding.tolist() for embedding in embeddings]


# logger.info(f"encode result: \n{request.input}")

# 计算token 数
def num_tokens_from_string(string: str) -> int:
"""
Returns the number of tokens in a text string.
use cl100k_base tokenizer
"""
encoding = tiktoken.get_encoding('cl100k_base')
num_tokens = len(encoding.encode(string))
return num_tokens

# embedding 接口返回数据格式
response = {
"data": [
{
"object": "embedding",
"embedding": embedding,
"index": index
}
for index, embedding in enumerate(embeddings)
],
"model": request.model,
"object": "list",
"usage": CompletionUsage(
prompt_tokens=sum(len(text.split()) for text in request.input),
completion_tokens=0,
total_tokens=sum(num_tokens_from_string(text) for text in request.input),
)
}
return response

如果你的机器性能有限,可以选用ollama这个框架来很快速的部署大模型api服务, 官网:https://ollama.com/, 这个平台提供了很多量化的模型和 一行命令部署API服务

1
2
3
4
# 安装
curl -fsSL https://ollama.com/install.sh | sh
# 拉取模型并部署, 这里拉取qwen2-7b instruct Q4量化,显存只需要4.4G
ollama run qwen2:7b-instruct # 启动服务并在11434端口开启api接口

api 客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from openai import OpenAI

client = OpenAI(
base_url = 'http://localhost:11434/v1',
api_key='ollama', # required, but unused
)

response = client.chat.completions.create(
model="qwen2:7b-instruct",
messages=[
{"role": "user", "content": "你好"}
]
)
print(response.choices[0].message.content)
# 输出: 你好!有什么问题我可以帮助你解答吗?

前端:

前端采用streamlit前端框架,也是一款易上手的大模型服务前端搭建框架。 以下是个简易的调用大模型聊天的demo服务。非常简单,也就几行代码。

1
2
3
pip installl streamlit # 1.安装包
streamlit run demo.py # 2. 运行前端
http://localhost:8501/ # 3. 打开浏览器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#  demo.py
from openai import OpenAI
import streamlit as st

st.title("LLM 聊天")

client = OpenAI(api_key='xxx', base_url="http://localhost:11434/v1")

if "openai_model" not in st.session_state:
st.session_state["openai_model"] = "ollama"

if "messages" not in st.session_state:
st.session_state.messages = []

for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])

if prompt := st.chat_input("你好?"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)

with st.chat_message("assistant"):
stream = client.chat.completions.create(
model=st.session_state["openai_model"],
messages=[
{"role": m["role"], "content": m["content"]}
for m in st.session_state.messages
],
stream=True,
)
response = st.write_stream(stream)
st.session_state.messages.append({"role": "assistant", "content": response})

demo效果:

另外还有一个点就是LLM调用重要的参数如何去选择(top_p, temprature, presence_penalty),我这边整理了几个核心参数的调整思路。 对应我们的这个分析任务,显然是以新闻资料为核心,寻求生成的确定性。

img_16.png

3.3.4 inference加速

大模型虽然效果优越,但是也因为它”大“,导致服务性能很低,在我们部署服务时,需要采取一定的策略对模型预测进行加速才能获得更好的体验。

经过调研选择了VLLM这个大模型推理加速框架。 它有几个优点:

1.社区活跃,模型支持很快
2.加速效果明显。基于虚拟内存和分页的思想, 采用page attention ,允许在非连续的内存空间内存储token,内存的利用率接近于最优
3.使用简单,两行命令即可部署。 示例如下

1
2
3
4
5
6
# vllm llama3 openai
# 下载vllm
pip install vllm
# 部署 一个兼容openai api接口的模型服务,端口8000
python -m vllm.entrypoints.openai.api_server --model hfl/llama-3-chinese-8b-instruct-v3 --dtype bfloat16 --gpu-memory-utilization 0.6 --chat-template llama3-instruct-template.jinja --enforce-eager --uvicorn-log-level warning --port 8000 --disable-log-stats --uvicorn-log-level warning

为了测试实际环境下的效果,我们运行了vllm的对比测试脚本

1
2
3
4
5
6
git clone https://github.com/vllm-project/vllm.git
cd vllm/benchmarks
# 测试vllm
python benchmark_throughput.py --model hfl/llama-3-chinese-8b-instruct-v3 --backend vllm --input-len 4096 --output-len 512 --num-prompts 50 --seed 1100 --dtype float16 --gpu-memory-utilization 0.6
# 测试HF
python benchmark_throughput.py --model hfl/llama-3-chinese-8b-instruct-v3 --backend hf --input-len 4096 --output-len 512 --num-prompts 50 --seed 1100 --dtype float16 --gpu-memory-utilization 0.6 --hf-max-batch-size 10

效果如下所示,可以看到单条inference 性能上,VLLM大约是HF的两倍, 但是当并发时,VLLM效果提升明显,吞吐量提升10倍。

img_17.png

当然,我们可以根据我们的显卡环境采取其他的加速方法,如

  • 输入输出优化。 如prompt 裁剪, 规整; 限制输出序列长度等
  • 模型优化。 模型压缩, 使用量化模型,使用更小参数模型等等

下面来看看整体效果的演示, 速度还是非常快的:

4. 总结

RAG的agent开发,入门还是比较简单的,现在市面上可用的框架也非常多,只需花费一些时间就能搭出一个可用的demo. 但是想要做的好,稳定服务,还是需要费很多的功夫去研究的,希望我的经验能给大家带来一些收获,少走一些弯路。

目前这个系统还不是很完善, 包括相关性判断,搜索意图判断等都有很大的优化空间。做这个东西的初衷是希望能在音乐热点的场景中进行应用,目前也已经在实践的过程中了,去辅助音乐热点的挖掘和运营。后续的话还希望添加的功能包括:

  • 音乐热点的识别与事件总结。
  • 结合云音乐站内知识做融合,分析。比如识别事件歌手,歌曲,原因,产出文案等等。

参考文献:

[1]. Azure AI Search: Outperforming vector search with hybrid retrieval and ranking capabilities

[2]. 【好玩儿的Docker项目】SearXNG

[3]. RAG 高效应用指南:Embedding 模型的选择和微调

[4]. ReRank 与 Embedding 模型的区别? 如何选择 ReRank 模型?

[5]. 【时代前沿】:单测场景下tempature、top_p、frequency_penalty、presence_penalty参数调整经验分享