diff --git a/README.md b/README.md index 3333990c..83f1f1b3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## Dockers 启动 -**提醒**:此部分暂时依赖于前端打包之后的内容(后面考虑更新),同时会自动启动 neo4j 图数据库。 +**提醒**:下面的脚本会启动开发版本,源代码的修改会自动更新(含前端和后端)。如果生产环境部署,请使用 ` docker/docker-compose.yml` 启动。 ```bash docker-compose -f docker/docker-compose.dev.yml up --build @@ -40,98 +40,37 @@ docker-compose -f docker/docker-compose.dev.yml up --build docker-compose -f docker/docker-compose.dev.yml down ``` +查看日志: + +```bash +docker logs # 例如:docker logs api-dev +``` + 如果需要使用到本地模型,比如向量模型或者重排序模型,则需要将环境变量中设置的 `MODEL_ROOT_DIR` 做映射,比如本地模型都是存放在 `/hdd/models` 里面,则需要在 `docker-compose.yml` 中添加: ```yml services: - # 后端服务 - backend: - image: pytorch/pytorch:2.4.1-cuda11.8-cudnn9-runtime # 或者您可以自定义 Python 基础镜像 - container_name: backend + api: + build: + context: .. + dockerfile: docker/api.Dockerfile + container_name: api-dev working_dir: /app volumes: - - ./src:/app/src # 映射源代码 - - ./requirements.txt:/app/requirements.txt - - ./saves:/app/saves - - /hdd/models:/hdd/models # <=== 修改这里 -... + - ../src:/app/src + - ../saves:/app/saves + - /hdd/zwj/models:/hdd/zwj/models # <== 修改这一行 ``` -## - -1. 配置 python 环境 `pip install -r requirements.txt`,python 版本应当小于 `3.12`。 -2. 前端 UI 部分,需要安装 Node.js 环境,参考:[Download Node.js](https://nodejs.org/en/download/package-manager)。 - -**如果不启用知识库,可以仅安装下面的依赖** - -``` -FlagEmbedding==1.2.10 -Flask==3.0.3 -Flask_Cors==4.0.1 -openai==1.35.10 -python-dotenv==1.0.1 -PyYAML==6.0.1 -zhipuai -``` - -> [!WARNING] -> milvus-lite(向量数据库)不支持在 Windows 上运行,建议使用 WSL,详见:https://github.com/milvus-io/milvus-lite/issues/175 - -### 配置图数据库 neo4j (可选) - -使用 docker 部署 neo4j 服务,配置文件见 [local_neo4j/docker-compose.yml](local_neo4j/docker-compose.yml). -默认账号密码见最后一行,可以使用 `http://localhost:7474/` 在浏览器可视化访问。 - -```bash -cd local_neo4j -docker compose up -d -``` - -可以使用 `python test_neo4j.py` 来测试是否正常启动。使用 `docker compose down` 可停止服务。 -如果想要管理 neo4j,也可以使用 `docker ps` 查看容器 id,然后使用 `docker exec -it /bin/bash` 进入容器。 -如果想要删除数据库中的文件,可以进入容器并停止 neo4j 后,执行 `rm -rf /data/databases`。 - -## 启动 - -推荐使用 docker 启动 - -### 1. 手动启动 - -```bash -# 后端部分 -python -m src.main - -# 前端部分 -cd web -npm install # 首次运行需要 -npm run server -``` - -### 2. 脚本启动 - -注意:此脚本不会启动图数据库 neo4j。 - -```bash -bash run.sh -``` - -### 3. Docker 启动 - -## Changelog +## 更新日志 - 2024.10.12 后端修改为 FastAPI,并添加了 Milnvs 的独立部署。 -## 其余脚本 +## 相关问题 -镜像构建之后,单独启动后端服务 +### 镜像下载问题 -```bash -docker run -w /app -v ./src:/app/src -v ./saves:/app/saves -p 5000:5000 yuxi-know-backend -``` - -## Docker 镜像相关 - -如果无法直接下载相关镜像,尝试将前缀替换为: +如果无法直接下载相关镜像,参考 [DaoCloud/public-image-mirror](https://github.com/DaoCloud/public-image-mirror?tab=readme-ov-file#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B),尝试将前缀替换为: ```bash # 以 neo4j 为例,其余一样 diff --git a/src/models/README.md b/src/models/README.md index b798a3c3..b9113589 100644 --- a/src/models/README.md +++ b/src/models/README.md @@ -13,24 +13,11 @@ |`deepseek`|`deepseek-chat`|`DEEPSEEK_API_KEY`| |`siliconflow` | `meta-llama/Meta-Llama-3.1-8B-Instruct` | `SILICONFLOW_API_KEY`| -vllm 的具体配置项可以参考[这里](https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html#named-arguments), 部署参考脚本: - -```bash -python -m vllm.entrypoints.openai.api_server \ - --model="/hdd/models/meta-llama/Meta-Llama-3.1-8B-Instruct" \ - --tensor-parallel-size 1 \ - --trust-remote-code \ - --device auto \ - --gpu-memory-utilization 0.98 \ - --dtype half \ - --served-model-name "vllm" \ - --host 0.0.0.0 \ - --port 8080 -``` +同样支持以 OpenAI 的兼容模型运行模型,可以直接在设置里面添加。比如使用 vllm 和 Ollama 运行本地模型时。 ### 2. 向量模型支持 -建议直接使用智谱 AI 的 embedding-3。 +建议直接使用智谱 AI 的 embedding-3,这样不需要做任何修改,且资费不贵。 > [!Warning] > 需要注意,由于知识库和图数据库的构建都依赖于向量模型,如果中途更改向量模型,回导致知识库不可用。此外,知识图谱的向量索引的建立默认使用 embedding-3 构建,因此检索的时候必须使用 embedding-3(现阶段还不支持修改) @@ -38,22 +25,23 @@ python -m vllm.entrypoints.openai.api_server \ |模型名称(`config.embed_model`)|默认路径/模型|需要配置项目(`config.model_local_paths`)| |:-|:-|:-| -|`bge-large-zh-v1.5`|`BAAI/bge-large-zh-v1.5`|`bge-large-zh-v1.5`(*修改为本地路径)| +|`bge-large-zh-v1.5`|`BAAI/bge-large-zh-v1.5`|`bge-large-zh-v1.5`(*可选:修改为本地路径)| |`zhipu`|`embedding-2`, `embedding-3`|`ZHIPUAI_API_KEY` (`.env`)| -例如(`saves/config/config.yaml`): - -```yaml -model_provider: qianfan -model_name: null # for default - -## model dir 可以写**相对路径**和**绝对路径** -### 相对路径是相对于环境变量 (.env) 中 MODEL_ROOT_DIR(若为空则是相对于 `pretrained_models`) 的路径 -model_local_paths: - bge-large-zh-v1.5: /models/bge-large-zh-v1.5 -``` - ### 3. 重排序模型支持 目前仅支持 `BAAI/bge-reranker-v2-m3`。 + +### 4. 本地模型支持 + +对于**语言模型**,并不支持直接运行本地语言模型,请使用 vllm 或者 ollama 转成 API 服务之后使用。 + +对于**向量模型**和**重排序模型**,可以不做修改会自动下载模型,如果下载过程中出现问题,请参考 [HF-Mirror](https://hf-mirror.com/) 配置相关内容。如果想要使用本地已经下载好的模型(不建议),可以在 `saves/config/config.yaml` 配置相关内容。同时注意要在 docker 中做映射,参考 README 中的 `docker/docker-compose.yml`。 + +例如: + +```yaml +model_local_paths: + bge-large-zh-v1.5: /models/bge-large-zh-v1.5 +``` diff --git a/src/routers/chat_router.py b/src/routers/chat_router.py index f7a9271c..9f50d2da 100644 --- a/src/routers/chat_router.py +++ b/src/routers/chat_router.py @@ -28,14 +28,30 @@ def chat_post( cur_res_id: str = Body(...)): history_manager = HistoryManager(history) - new_query, refs = startup.retriever(query, history_manager.messages, meta) - refs_pool[cur_res_id] = refs - messages = history_manager.get_history_with_msg(new_query, max_rounds=meta.get('history_round')) - history_manager.add_user(query) - logger.debug(f"Web history: {history_manager.messages}") + def make_chunk(content, status, history): + return json.dumps({ + "response": content, + "history": history, + "model_name": startup.config.model_name, + "status": status, + }, ensure_ascii=False).encode('utf-8') + b"\n" def generate_response(): + + if meta.get("enable_retrieval"): + chunk = make_chunk("", "searching", history=None) + yield chunk + + new_query, refs = startup.retriever(query, history_manager.messages, meta) + refs_pool[cur_res_id] = refs + else: + new_query = query + + messages = history_manager.get_history_with_msg(new_query, max_rounds=meta.get('history_round')) + history_manager.add_user(query) + logger.debug(f"Web history: {history_manager.messages}") + content = "" for delta in startup.model.predict(messages, stream=True): if not delta.content: @@ -47,12 +63,8 @@ def chat_post( content += delta.content logger.debug(f"Response: {content}") - - _chunk = json.dumps({ - "response": content, - "history": history_manager.update_ai(content), - }, ensure_ascii=False).encode('utf-8') + b"\n" - yield _chunk + chunk = make_chunk(content, "loading", history=history_manager.update_ai(content)) + yield chunk return StreamingResponse(generate_response(), media_type='application/json') diff --git a/web/src/components/ChatComponent.vue b/web/src/components/ChatComponent.vue index 40d84b8c..80dcf95c 100644 --- a/web/src/components/ChatComponent.vue +++ b/web/src/components/ChatComponent.vue @@ -16,10 +16,10 @@
-
-
+
+ 流式输出
+
+
+ 总结对话标题
+
+
+ 启用检索
+
+
+ 最大历史轮数 +
+ +
知识库
@@ -62,24 +75,15 @@
-
+
图数据库
-
+
搜索引擎(Bing)
-
- 流式输出
-
-
- 总结对话标题
-
-
+
重写查询
-
- 最大历史轮数 -
@@ -104,12 +108,19 @@ :class="message.role" >

{{ message.text }}

-
+
-
请求错误,请重试
+
正在检索……
+
+ 请求错误,请重试 +
{ role: 'received', text: message, refs, - status: "querying", + status: "init", }) scrollToBottom() } -const updateMessage = (text, id, refs, status) => { - const message = conv.value.messages.find((message) => message.id === id); +const updateMessage = (info) => { + const message = conv.value.messages.find((message) => message.id === info.id); if (message) { // 只有在 text 不为空时更新 - if (text !== null && text !== undefined && text !== '') { - message.text = text; + if (info.text !== null && info.text !== undefined && info.text !== '') { + message.text = info.text; } // 只有在 refs 不为空时更新 - if (refs !== null && refs !== undefined) { - message.refs = refs; + if (info.refs !== null && info.refs !== undefined) { + message.refs = info.refs; + } - // 如果 refs 里面的 model_name 不为空时更新 - if (refs.model_name !== null && refs.model_name !== undefined && refs.model_name !== '') { - message.model_name = refs.model_name; - } + if (info.model_name !== null && info.model_name !== undefined && info.model_name !== '') { + message.model_name = info.model_name; } // 只有在 status 不为空时更新 - if (status !== null && status !== undefined && status !== '') { - message.status = status; + if (info.status !== null && info.status !== undefined && info.status !== '') { + message.status = info.status; } } else { console.error('Message not found'); @@ -412,11 +423,21 @@ const fetchChatResponse = (user_input, cur_res_id) => { const readChunk = () => { return reader.read().then(({ done, value }) => { if (done) { - fetchRefs(cur_res_id).then((data) => { - console.log(data) - updateMessage(null, cur_res_id, data, "finished"); + const message = conv.value.messages.find((message) => message.id === cur_res_id) + if (message.refs && message.refs.meta.enable_retrieval) { + console.log("fetching refs") + fetchRefs(cur_res_id).then((data) => { + console.log(data) + updateMessage({ + id: cur_res_id, + refs: data, + status: "finished", + }); + updateStatus(cur_res_id, "finished"); + }) + } else { updateStatus(cur_res_id, "finished"); - }) + } isStreaming.value = false; if (conv.value.messages.length === 2) { renameTitle(); } return; @@ -426,9 +447,16 @@ const fetchChatResponse = (user_input, cur_res_id) => { buffer += chunk; try { const data = JSON.parse(chunk); - updateMessage(data.response, cur_res_id, data.refs, "loading"); + updateMessage({ + id: cur_res_id, + text: data.response, + model_name: data.model_name, + status: data.status, + }); console.debug(data.response) - conv.value.history = data.history; + if (data.history) { + conv.value.history = data.history; + } } catch (e) { // console.debug('JSON 解析错误:', e, chunk); } @@ -444,9 +472,6 @@ const fetchChatResponse = (user_input, cur_res_id) => { updateStatus(cur_res_id, "error"); isStreaming.value = false; }) - .finally(() => { - isStreaming.value = false; - }); } const fetchRefs = (cur_res_id) => { @@ -484,6 +509,19 @@ const sendMessage = () => { } } +const retryMessage = (id) => { + // 找到 id 对应的 message,然后删除包含 message 在内以及后面所有的 message + console.log("retryMessage", id) + const index = conv.value.messages.findIndex(message => message.id === id); + const pastMessage = conv.value.messages[index-1] + conv.value.inputText = pastMessage.text + if (index !== -1) { + conv.value.messages = conv.value.messages.slice(0, index-1); + } + console.log(conv.value.messages) + sendMessage(); +} + const autoSend = (message) => { conv.value.inputText = message sendMessage() @@ -677,6 +715,12 @@ watch( border-radius: 8px; text-align: center; background: #FFF0F0; + margin-bottom: 10px; + cursor: pointer; + } + + .searching-msg { + color: var(--gray-500); } }