新闻聊天:使用Chroma、Gemini Flash 1.5、Text Embedding 004和Cloudflare隧道实施RAG。
几个月前,我开发了一款新闻摘要器和情感指示器,可以处理来自一家知名报纸的文章,利用本地的大型语言模型(LLMs)的力量。通过利用Ollama和Llama 3,我能够在配备苹果芯片的Mac Mini上高效运行此系统。从那时起,我已经收集了近1.5万篇文章。有了这个丰富的数据集,我决定构建一个聊天机器人,可以回答问题并提供我真正感兴趣的新闻的见解,根据需求提供个性化的新闻摘要和情感分析。
遵循相同的原则,我旨在使这个项目尽可能具有成本效益,依赖于本地处理能力而不是昂贵的基于云的基础设施,确保我保持对数据和开支的控制。
与新闻聊天
该聊天功能可在https://news.misprops.co上使用,由于聊天完成API在我家的一台小型Mac mini上运行,可能并不总是在线。该聊天功能也是用西班牙语的 :)
工具
- Chroma https://www.trychroma.com/ 色度 https://www.trychroma.com/
- 雙子座Flash 1.5 https://deepmind.google/technologies/gemini/flash/
- 文本嵌入 004 https://ai.google.dev/gemini-api/docs/embeddings
- 云闪Tunnel https://www.cloudflare.com/en-au/products/tunnel/
- 表达 https://expressjs.com/
- React 简单聊天机器人https://lucasbassetti.com.br/react-simple-chatbot/
Github Github
请访问https://github.com/lomaky/news-analyser.
在docker上安装Chroma
以下命令运行一个chroma容器,将数据库映射到主机计算机,并将流量重定向到端口8000。
docker run -d --name chromadb -v ./chroma:/path/on/host -p 8000:8000 -e IS_PERSISTENT=TRUE -e ANONYMIZED_TELEMETRY=TRUE chromadb/chroma:latest
使用文本嵌入将新闻文章向量化 004
为什么选择文本嵌入004?
尽管Chroma提供了自己的嵌入,不需要GPU,但它们往往非常慢,而且也是量化的,这可能会影响它们的性能和准确性。相比之下,文本嵌入004是一个免费、高质量的选择,提供更快、更准确的结果。它旨在改进各种谷歌AI产品,使其成为嵌入任务的强大替代方案。要开始使用文本嵌入004,可以通过访问以下链接获取Google AI密钥:
https://aistudio.google.com/app/apikey https://aistudio.google.com/app/apikey
在这个项目中,文章已经以以下格式存储在本地的JSON文件中:
{
"title": "Bogota, asi sera la ciudad en la que todos podremos caminar seguros |opinion",
"date": "2024-06-11T00:00:00.000Z",
"id": "3351364",
"category": "Bogota",
"url": "https://www.eltiempo.com/bogota/bogota-asi-sera-la-ciudad-en-la-que-todos-podremos-caminar-seguros-opinion-3351364",
"content": "Entendiendo los desafíos que hoy tiene Bogotá para ser la ciudad en la que todos queramos caminar, el Sector Movilidad, con un presupuesto de 19,8 billones de pesos, tiene la responsabilidad de liderar y gestionar 43 metas del Plan Distrital de Desarrollo ‘Bogotá Camina Segura’ 2024-2027.Con más andenes, ciclorrutas, vías, cables aéreos, troncales de TransMilenio y el Metro, en la Administración del alcalde Carlos Fernando Galán, trabajamos para construir una movilidad que esté en armonía entre todos los actores viales y con el medio ambiente. Es por ello que durante estos 4 años nos dedicaremos a implementar los grandes proyectos de infraestructura vial y de transporte público, con más de 60 proyectos de infraestructura que beneficiarán a más de 6 millones de habitantes, y asumimos el compromiso histórico de intervenir el 40 % de la malla vial en mal estado.Las entidades del Sector Movilidad trabajaremos por una ciudad que logre el bien-estar para todos y todas. Para eso ofreceremos más y mejores opciones de movilidad saludables y seguras, con 83 mil cupos de cicloparqueaderos públicos y privados; seguiremos mejorando los viajes de los más de 480 mil estudiantes del programa Niñas y Niños Primero; construiremos 59 km nuevos de ciclorrutas; e implementaremos 80 km/carril nuevos de malla vial. Una de las grandes apuestas será la construcción de la ALO Norte, una obra esperada por décadas por todos los habitantes del occidente de la capital.Como Secretaria de Movilidad, mi prioridad es proteger y salvar vidas en las vías y recuperar la confianza de los ciudadanos, tener un sistema de movilidad más eficiente y confiable, generar más viajes en modos sostenibles, mejorar el servicio de transporte público, y seguir afianzando a Bogotá como una ciudad global competitiva y conectada.Para eso estamos trabajando y caminaremos las 20 localidades para escuchar y conocer las necesidades de sus habitantes, implementando las estrategias y programas establecidas en el Plan de Desarrollo para mejorar el acceso y la seguridad de todos los usuarios de las vías, en especial, de las personas más vulnerables.Con eficiencia, inclusión y sostenibilidad, Bogotá caminará segura.Claudia DíazSecretaria de Movilidad de Bogotá",
"thumbnail": "https://imagenes.eltiempo.com/files/image_1200_600/uploads/2024/06/11/66686e1a7ff21.jpeg",
"summary": "Aquí te dejo una resumen de la noticia en español:\n\nLa Secretaría de Movilidad de Bogotá, liderada por Claudia Díaz, ha presentado el Plan Distrital de Desarrollo \"Bogotá Camina Segura\" 2024-2027, con un presupuesto de 19.8 billones de pesos. El objetivo es construir una movilidad en armonía entre todos los actores viales y con el medio ambiente. Durante los próximos 4 años, se implementarán más de 60 proyectos de infraestructura vial y transporte público que beneficiarán a más de 6 millones de habitantes.\n\nEntre las metas del plan se encuentran:\n\n* Construir 59 km nuevos de ciclorrutas\n* Implementar 80 km/carril nuevos de malla vial\n* Mejorar los viajes de los estudiantes del programa Niñas y Niños Primero\n* Ofrecer más y mejores opciones de movilidad saludables y seguras\n* Proteger y salvar vidas en las vías\n\nLa Secretaria de Movilidad ha destacado que su prioridad es proteger y salvar vidas en las vías, recuperar la confianza de los ciudadanos y afianzar a Bogotá como una ciudad global competitiva y conectada. Para lograr este objetivo, se están trabajando estrategias y programas para mejorar el acceso y la seguridad de todos los usuarios de las vías, especialmente de las personas más vulnerables.",
"sentiment": "Positiva",
"positive": true
}
将文章以这种格式存储的优点是可以直接将其向量化,而无需将其拆分为更小的块。这对于检索增强生成任务(RAG)特别有用,其中搜索结果的准确性很大程度上取决于块状策略。不恰当的块状化可能导致关键上下文或见解丢失,降低检索效果。通过以结构化格式处理整个文档,更容易保持语义完整性,并在生成响应时提高搜索结果的质量。
以下代码读取全部文章,生成嵌入并将它们存储在chroma中。完整完成所有操作需要几个小时,有时我会遇到谷歌嵌入API的速率限制。
const fs = require("fs");
const path = require("path");
import { ChromaClient, GoogleGenerativeAiEmbeddingFunction } from "chromadb";
const main = async () => {
// Article news
const articlesRelativePath = "../news-processor/articles/";
const articlesPath = path.join(__dirname, articlesRelativePath);
console.log(articlesPath);
// embeddings
const googleKey = "YOUR_GOOGLE_GEMINI_KEY";
const googleEmbeddings = new GoogleGenerativeAiEmbeddingFunction({
googleApiKey: googleKey,
model: "text-embedding-004",
});
// VectorDb
const client = new ChromaClient({
path: "http://localhost:8000",
});
const vectorDbName = `news-text-embedding-004.vdb`;
// Get or create new VectorDB collection
const vectorDb = await client.getOrCreateCollection({
name: vectorDbName,
embeddingFunction: googleEmbeddings,
});
const files = fs
.readdirSync(articlesPath)
.filter((file) => path.extname(file) === ".json");
for (const path of files) {
const file = `${articlesRelativePath}${path}`;
console.log(file);
const data = fs.readFileSync(file);
const article = JSON.parse(data) as Article;
if (
article &&
article.id &&
article.content &&
article.title &&
article.date
) {
const articleExists = await vectorDb.get({
ids: [article.id!.toString()],
});
if (!articleExists || articleExists.ids.length < 1) {
// Vectorize article
await vectorDb.upsert({
ids: [article.id!.toString()],
documents: [article.content!],
metadatas: [
{
title: article.title!,
date: new Date(article.date!).toISOString(),
url: article.url ?? "",
},
],
});
console.log(`Vectorized: ${article.title!}`);
await new Promise((resolve) => setTimeout(resolve, 200));
} else {
console.log(`Already vectorized: ${article.title!}`);
}
}
}
};
export interface Article {
title?: string;
date?: Date;
id?: string;
category?: string;
url?: string;
content?: string;
summary?: string;
positive?: boolean;
sentiment?: string;
weight?: number;
thumbnail?: string;
}
main();
查询RAG并使用Gemini Flash 1.5组合响应
为什么选择双子座Flash?
我使用Gemini Flash是因为它是一个来自谷歌的免费生成式人工智能选项,它有一个1m的巨大上下文窗口和一个8k的输出窗口。然而,它带有速率限制:
每分钟15次请求(RPM)。
每月100万令牌(TPM)。
这些速率限制使 Gemini Flash 成为相对低至中等使用量项目的理想经济选择。但是,对于更频繁或更强度使用的情况,您可能需要订阅他们的服务。
这是用于聊天完成API的代码,它设置了一个Express服务器,提供检索增强生成(RAG)搜索的REST API。它结合了谷歌的“宝石闪光”模型用于文本生成与RAG结果,使系统能够根据检索到的信息生成上下文相关的响应。
const express = require("express");
var cors = require("cors");
import { HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
import { ChromaClient, GoogleGenerativeAiEmbeddingFunction } from "chromadb";
const { GoogleGenerativeAI } = require("@google/generative-ai");
const queryRag = async (query: string) => {
const googleKey = "YOUR_GOOGLE_GEMINI_KEY";
// embeddings
const googleEmbeddings = new GoogleGenerativeAiEmbeddingFunction({
googleApiKey: googleKey,
model: "text-embedding-004",
});
// VectorDb
const client = new ChromaClient({
path: "http://localhost:8000",
});
const vectorDbName = `news-text-embedding-004.vdb`;
console.log(`VectorDb=${vectorDbName}`);
// Get or create new VectorDB collection
const vectorDb = await client.getOrCreateCollection({
name: vectorDbName,
embeddingFunction: googleEmbeddings,
});
// Query
const results = await vectorDb.query({
queryTexts: [query],
nResults: 20,
});
// Compose response
const genAI = new GoogleGenerativeAI(googleKey);
const safetySettings = [
{
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold: HarmBlockThreshold.BLOCK_NONE,
},
{
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold: HarmBlockThreshold.BLOCK_NONE,
},
{
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold: HarmBlockThreshold.BLOCK_NONE,
},
{
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold: HarmBlockThreshold.BLOCK_NONE,
},
];
const model = genAI.getGenerativeModel({
model: "gemini-1.5-flash",
safetySettings: safetySettings,
});
const prompt = `
<INSTRUCCIONES DEL PROMPT>
Eres un agente que busca noticias y responde a los usuarios.
Responde la siguente pregunta de un usuario usando el resultado de la busqueda a continuacion.
Usa un lenguaje amigable e impersonal.
Omite links a paginas web.
Limitate a solo responder y no hacer preguntas adicionales.
</INSTRUCCIONES DEL PROMPT>
<PREGUNTA DEL USUARIO>
${query}
</PREGUNTA DEL USUARIO>
<RESULTADOS BUSQUEDA NOTICIAS>
${JSON.stringify(results)}
</RESULTADOS BUSQUEDA NOTICIAS>
`;
const result = await model.generateContent(prompt);
const response = result.response.text();
console.log(response);
return response;
};
const app = express();
app.use(express.json());
const PORT = 9700;
app.listen(PORT, () => {
console.log("Server Listening on port:", PORT);
});
app.get("/search", cors(), async (request, response) => {
const dbResponse = await queryRag(request.query.query);
const ragResponse = {
Query: request.query.query,
Response: dbResponse,
};
response.send(ragResponse);
});
为了使这个 Express 服务器可以通过互联网访问,我正在利用 Cloudflare 隧道,这样可以安全地将内部网络中的端口暴露到互联网上。这种方法简化了外部访问,无需复杂的网络配置。关于如何设置 Cloudflare 隧道的逐步指南,请参考这篇文章:https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/
网络客户端
对于网络聊天客户端,我使用了一个简单的UI,由https://lucasbassetti.com/编写,它与我的聊天完成API进行交互。
import React, { Component } from "react";
import PropTypes from "prop-types";
import ChatBot, { Loading } from "react-simple-chatbot";
import { ThemeProvider } from "styled-components";
import tw from "twin.macro";
const AIResponse = tw.div`text-xs`;
class LomakyVectorDB extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
result: "",
trigger: false,
};
this.triggerNext = this.triggerNext.bind(this);
}
componentDidMount() {
const self = this;
const { steps } = this.props;
const search = steps.search.value;
const queryUrl = `https://completion.cheapdomain.store/search?query=${search}`;
const xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", function() {
if (this.readyState === 4) {
const data = JSON.parse(this.responseText);
const response = data.Response;
if (response && response) {
self.setState({ loading: false, result: response }, () => {
// Trigger next step after displaying response
self.triggerNext();
});
} else {
self.setState(
{
loading: false,
result:
"No encontramos respuesta a tu pregunta, intenta de nuevo.",
},
() => {
// Trigger next step after displaying response
self.triggerNext();
}
);
}
}
});
xhr.open("GET", queryUrl);
xhr.send();
}
triggerNext() {
this.setState({ trigger: true }, () => {
this.props.triggerNextStep();
});
}
render() {
const { loading, result } = this.state;
return <AIResponse>{loading ? <Loading /> : result}</AIResponse>;
}
}
LomakyVectorDB.propTypes = {
steps: PropTypes.object,
triggerNextStep: PropTypes.func,
};
LomakyVectorDB.defaultProps = {
steps: undefined,
triggerNextStep: undefined,
};
const CHATBOT_THEME = {
background: "#FFFEFC",
headerBgColor: "#6415ff",
headerFontColor: "#fff",
headerFontSize: "15px",
botBubbleColor: "#6415ff",
botFontColor: "#fff",
userBubbleColor: "#fff",
userFontColor: "#4a4a4a",
};
const ChatBotHelper = () => {
const steps = [
{
id: "1",
message:
"Hola, te puedo ayudar a resolver preguntas de noticias anteriores, ¿Qué quisieras saber?",
trigger: "search",
},
{
id: "search",
user: true,
trigger: "3",
},
{
id: "3",
component: <LomakyVectorDB />,
waitAction: true,
trigger: "4",
},
{
id: "4",
message: "¿Hay algo más que te gustaría saber?",
trigger: "search",
},
];
return (
<>
<ThemeProvider theme={CHATBOT_THEME}>
<ChatBot
steps={steps}
floating={true}
headerTitle="Habla con las noticias"
enableSmoothScroll={true}
/>
</ThemeProvider>
</>
);
};
export default ChatBotHelper;
未来的改进
这个实现是一个概念验证(PoC),不适合生产使用。然而,它提供了一个简单明了的演示,展示了RAG系统的工作原理。有几个潜在的改进可以实现,比如将GraphRAG集成起来更好地理解不同块之间的关系,加入重新排序以获得更准确的结果,并优化整体架构以提高效率和可扩展性。