细节优化

This commit is contained in:
Wenjie Zhang 2025-02-23 16:39:52 +08:00
parent b2dbc17fec
commit 19a8e4714e
7 changed files with 158 additions and 150 deletions

View File

@ -27,6 +27,7 @@ class GraphDatabase:
self.kgdb_name = kgdb_name self.kgdb_name = kgdb_name
assert embed_model, "embed_model=None" assert embed_model, "embed_model=None"
self.embed_model = embed_model self.embed_model = embed_model
self.embed_model_name = None
def start(self): def start(self):
uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687") uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
@ -87,7 +88,8 @@ class GraphDatabase:
"relationship_count": relationship_count, "relationship_count": relationship_count,
"triples_count": triples_count, "triples_count": triples_count,
"labels": labels, "labels": labels,
"status": self.status "status": self.status,
"embed_model_name": self.embed_model_name
} }
with self.driver.session() as session: with self.driver.session() as session:
@ -171,6 +173,7 @@ class GraphDatabase:
def jsonl_file_add_entity(self, file_path, kgdb_name='neo4j'): def jsonl_file_add_entity(self, file_path, kgdb_name='neo4j'):
self.status = "processing" self.status = "processing"
kgdb_name = kgdb_name or 'neo4j' kgdb_name = kgdb_name or 'neo4j'
self.embed_model_name = self.embed_model_name or self.config.embed_model
self.use_database(kgdb_name) # 切换到指定数据库 self.use_database(kgdb_name) # 切换到指定数据库
def read_triples(file_path): def read_triples(file_path):

View File

@ -2,18 +2,20 @@ import os
import requests import requests
import numpy as np import numpy as np
from typing import List, Union, Dict from typing import List, Union, Dict
from src.models.embedding import RemoteEmbeddingModel
from src.utils.logging_config import setup_logger from src.utils.logging_config import setup_logger
logger = setup_logger("OllamaEmbedding") logger = setup_logger("OllamaEmbedding")
class OllamaEmbedding: class OllamaEmbedding(RemoteEmbeddingModel):
""" """
使用 Ollama API 进行文本嵌入的类 使用 Ollama API 进行文本嵌入的类
""" """
def __init__(self, model_info: Dict, config) -> None: def __init__(self, model_info: Dict, config) -> None:
""" """
初始化 Ollama Embedding 模型 初始化 Ollama Embedding 模型
Args: Args:
model_info: 模型信息字典 model_info: 模型信息字典
config: 配置对象 config: 配置对象
@ -28,10 +30,10 @@ class OllamaEmbedding:
def _get_embedding(self, text: str) -> List[float]: def _get_embedding(self, text: str) -> List[float]:
""" """
获取单个文本的嵌入向量 获取单个文本的嵌入向量
Args: Args:
text: 输入文本 text: 输入文本
Returns: Returns:
嵌入向量 嵌入向量
""" """
@ -50,10 +52,10 @@ class OllamaEmbedding:
def predict(self, messages: List[str]) -> List[List[float]]: def predict(self, messages: List[str]) -> List[List[float]]:
""" """
批量获取文本嵌入向量 批量获取文本嵌入向量
Args: Args:
messages: 文本列表 messages: 文本列表
Returns: Returns:
嵌入向量列表 嵌入向量列表
""" """
@ -63,23 +65,23 @@ class OllamaEmbedding:
for i in range(0, len(messages), batch_size): for i in range(0, len(messages), batch_size):
batch = messages[i:i + batch_size] batch = messages[i:i + batch_size]
logger.info(f"Processing batch {i//batch_size + 1}, size: {len(batch)}") logger.info(f"Processing batch {i//batch_size + 1}, size: {len(batch)}")
batch_embeddings = [] batch_embeddings = []
for text in batch: for text in batch:
embedding = self._get_embedding(text) embedding = self._get_embedding(text)
batch_embeddings.append(embedding) batch_embeddings.append(embedding)
embeddings.extend(batch_embeddings) embeddings.extend(batch_embeddings)
return embeddings return embeddings
def encode(self, messages: Union[str, List[str]]) -> List[List[float]]: def encode(self, messages: Union[str, List[str]]) -> List[List[float]]:
""" """
编码文本 编码文本
Args: Args:
messages: 单个文本或文本列表 messages: 单个文本或文本列表
Returns: Returns:
嵌入向量列表 嵌入向量列表
""" """
@ -90,10 +92,10 @@ class OllamaEmbedding:
def encode_queries(self, queries: List[str]) -> List[List[float]]: def encode_queries(self, queries: List[str]) -> List[List[float]]:
""" """
编码查询文本 编码查询文本
Args: Args:
queries: 查询文本列表 queries: 查询文本列表
Returns: Returns:
查询文本的嵌入向量列表 查询文本的嵌入向量列表
""" """
@ -107,7 +109,7 @@ class OllamaReranker:
def __init__(self, config) -> None: def __init__(self, config) -> None:
""" """
初始化 Ollama Reranker 初始化 Ollama Reranker
Args: Args:
config: 配置对象 config: 配置对象
""" """
@ -119,16 +121,16 @@ class OllamaReranker:
def compute_score(self, query: str, passage: str) -> float: def compute_score(self, query: str, passage: str) -> float:
""" """
计算查询和文本段落之间的相关性分数 计算查询和文本段落之间的相关性分数
Args: Args:
query: 查询文本 query: 查询文本
passage: 段落文本 passage: 段落文本
Returns: Returns:
相关性分数 相关性分数
""" """
prompt = f"Query: {query}\nPassage: {passage}\nRate the relevance of the passage to the query on a scale of 0 to 1:" prompt = f"Query: {query}\nPassage: {passage}\nRate the relevance of the passage to the query on a scale of 0 to 1:"
try: try:
response = requests.post( response = requests.post(
f"{self.base_url}/api/generate", f"{self.base_url}/api/generate",
@ -139,7 +141,7 @@ class OllamaReranker:
} }
) )
response.raise_for_status() response.raise_for_status()
# 提取生成的数字作为分数 # 提取生成的数字作为分数
result = response.json()["response"].strip() result = response.json()["response"].strip()
try: try:
@ -148,7 +150,7 @@ class OllamaReranker:
except ValueError: except ValueError:
logger.warning(f"Could not parse score from response: {result}") logger.warning(f"Could not parse score from response: {result}")
return 0.0 return 0.0
except Exception as e: except Exception as e:
logger.error(f"Error computing rerank score: {str(e)}") logger.error(f"Error computing rerank score: {str(e)}")
return 0.0 return 0.0
@ -156,12 +158,12 @@ class OllamaReranker:
def rerank(self, query: str, passages: List[str], top_n: int = None) -> List[Dict]: def rerank(self, query: str, passages: List[str], top_n: int = None) -> List[Dict]:
""" """
重新排序文本段落 重新排序文本段落
Args: Args:
query: 查询文本 query: 查询文本
passages: 段落文本列表 passages: 段落文本列表
top_n: 返回前 n 个结果 top_n: 返回前 n 个结果
Returns: Returns:
排序后的结果列表每个元素包含索引和分数 排序后的结果列表每个元素包含索引和分数
""" """
@ -169,11 +171,11 @@ class OllamaReranker:
for i, passage in enumerate(passages): for i, passage in enumerate(passages):
score = self.compute_score(query, passage) score = self.compute_score(query, passage)
scores.append({"index": i, "score": score}) scores.append({"index": i, "score": score})
# 按分数降序排序 # 按分数降序排序
sorted_results = sorted(scores, key=lambda x: x["score"], reverse=True) sorted_results = sorted(scores, key=lambda x: x["score"], reverse=True)
if top_n: if top_n:
sorted_results = sorted_results[:top_n] sorted_results = sorted_results[:top_n]
return sorted_results return sorted_results

View File

@ -27,26 +27,29 @@ def chat_post(
history_manager = HistoryManager(history) history_manager = HistoryManager(history)
def make_chunk(content=None, status=None, history=None, reasoning_content=None): def make_chunk(content=None, **kwargs):
return json.dumps({ return json.dumps({
"response": content, "response": content,
"reasoning_response": reasoning_content,
"history": history,
"model_name": startup.config.model_name, "model_name": startup.config.model_name,
"status": status,
"meta": meta, "meta": meta,
**kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n" }, ensure_ascii=False).encode('utf-8') + b"\n"
def generate_response(): def generate_response():
modified_query = query modified_query = query
refs = None
# 处理知识库检索 # 处理知识库检索
if meta and meta.get("enable_retrieval"): if meta and meta.get("enable_retrieval"):
chunk = make_chunk(status="searching") chunk = make_chunk(status="searching")
yield chunk yield chunk
modified_query, refs = startup.retriever(modified_query, history_manager.messages, meta) try:
refs_pool[cur_res_id] = refs modified_query, refs = startup.retriever(modified_query, history_manager.messages, meta)
except Exception as e:
logger.error(f"Retriever error: {e}")
yield make_chunk(message=f"Retriever error: {e}", status="error")
return
messages = history_manager.get_history_with_msg(modified_query, max_rounds=meta.get('history_round')) messages = history_manager.get_history_with_msg(modified_query, max_rounds=meta.get('history_round'))
history_manager.add_user(query) # 注意这里使用原始查询 history_manager.add_user(query) # 注意这里使用原始查询
@ -56,7 +59,7 @@ def chat_post(
reasoning_content = "" reasoning_content = ""
for delta in startup.model.predict(messages, stream=True): for delta in startup.model.predict(messages, stream=True):
if not delta.content and hasattr(delta, 'reasoning_content'): if not delta.content and hasattr(delta, 'reasoning_content'):
reasoning_content += delta.reasoning_content reasoning_content += delta.reasoning_content or ""
chunk = make_chunk(reasoning_content=reasoning_content, status="reasoning") chunk = make_chunk(reasoning_content=reasoning_content, status="reasoning")
yield chunk yield chunk
continue continue
@ -67,14 +70,15 @@ def chat_post(
else: else:
content += delta.content or "" content += delta.content or ""
chunk = make_chunk(content=content, chunk = make_chunk(content=content, status="loading")
reasoning_content=reasoning_content,
status="loading",
history=history_manager.update_ai(content))
yield chunk yield chunk
logger.debug(f"Final response: {content}") logger.debug(f"Final response: {content}")
logger.debug(f"Final reasoning response: {reasoning_content}") logger.debug(f"Final reasoning response: {reasoning_content}")
yield make_chunk(content=content,
status="finished",
history=history_manager.update_ai(content),
refs=refs)
return StreamingResponse(generate_response(), media_type='application/json') return StreamingResponse(generate_response(), media_type='application/json')

View File

@ -16,11 +16,11 @@ class WebSearcher:
def search(self, query: str, max_results: int = 1) -> List[Dict]: def search(self, query: str, max_results: int = 1) -> List[Dict]:
""" """
使用 Tavily 搜索相关内容 使用 Tavily 搜索相关内容
Args: Args:
query: 搜索查询 query: 搜索查询
max_results: 最大返回结果数 max_results: 最大返回结果数
Returns: Returns:
搜索结果列表 搜索结果列表
""" """
@ -30,7 +30,7 @@ class WebSearcher:
search_depth="basic", search_depth="basic",
max_results=max_results max_results=max_results
) )
# 提取需要的信息 # 提取需要的信息
formatted_results = [] formatted_results = []
for result in search_results['results'][:max_results]: for result in search_results['results'][:max_results]:
@ -40,9 +40,9 @@ class WebSearcher:
'url': result.get('url', ''), 'url': result.get('url', ''),
'score': result.get('score', 0) 'score': result.get('score', 0)
}) })
return formatted_results return formatted_results
except Exception as e: except Exception as e:
logger.error(f"Error during web search: {str(e)}") logger.error(f"Error during web search: {str(e)}")
return [] return []
@ -50,20 +50,20 @@ class WebSearcher:
def format_search_results(self, results: List[Dict]) -> str: def format_search_results(self, results: List[Dict]) -> str:
""" """
将搜索结果格式化为文本 将搜索结果格式化为文本
Args: Args:
results: 搜索结果列表 results: 搜索结果列表
Returns: Returns:
格式化后的文本 格式化后的文本
""" """
if not results: if not results:
return "没有找到相关的网络搜索结果。" return "没有找到相关的网络搜索结果。"
formatted_text = "以下是相关的网络搜索结果:\n\n" formatted_text = "以下是相关的网络搜索结果:\n\n"
for i, result in enumerate(results, 1): for i, result in enumerate(results, 1):
formatted_text += f"{i}. {result['title']}\n" formatted_text += f"{i}. {result['title']}\n"
formatted_text += f" {result['content']}\n" formatted_text += f" {result['content']}\n"
formatted_text += f" 来源: {result['url']}\n\n" formatted_text += f" 来源: {result['url']}\n\n"
return formatted_text return formatted_text

View File

@ -1,95 +1,95 @@
<template> <template>
<div class="graph-container" ref="container"></div> <div class="graph-container" ref="container"></div>
</template> </template>
<script setup> <script setup>
import { Graph } from "@antv/g6"; import { Graph } from "@antv/g6";
import { onMounted, watch, ref } from 'vue'; import { onMounted, watch, ref } from 'vue';
const props = defineProps({ const props = defineProps({
graphData: { graphData: {
type: Object, type: Object,
required: true, required: true,
default: () => ({ nodes: [], edges: [] }) default: () => ({ nodes: [], edges: [] })
}
});
const container = ref(null);
let graphInstance = null;
const initGraph = () => {
graphInstance = new Graph({
container: container.value,
width: container.value.offsetWidth,
height: container.value.offsetHeight,
autoFit: true,
autoResize: true,
layout: {
type: 'd3-force',
preventOverlap: true,
kr: 20,
collide: {
strength: 1.0,
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
size: 70,
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
endArrow: true,
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
};
const renderGraph = () => {
if (!graphInstance) {
initGraph();
}
const formattedData = {
nodes: props.graphData.nodes.map(node => ({
id: node.id,
data: { label: node.name }
})),
edges: props.graphData.edges.map(edge => ({
source: edge.source_id,
target: edge.target_id,
data: { label: edge.type }
}))
};
graphInstance.setData(formattedData);
graphInstance.render();
};
onMounted(() => {
renderGraph();
window.addEventListener('resize', renderGraph);
});
watch(() => props.graphData, renderGraph, { deep: true });
</script>
<style scoped>
.graph-container {
background: #F7F7F7;
border-radius: 16px;
width: 100%;
height: 600px;
overflow: hidden;
} }
</style> });
const container = ref(null);
let graphInstance = null;
const initGraph = () => {
graphInstance = new Graph({
container: container.value,
width: container.value.offsetWidth,
height: container.value.offsetHeight,
autoFit: true,
autoResize: true,
layout: {
type: 'd3-force',
preventOverlap: true,
kr: 20,
collide: {
strength: 1.0,
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
size: 70,
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
endArrow: true,
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
};
const renderGraph = () => {
if (!graphInstance) {
initGraph();
}
const formattedData = {
nodes: props.graphData.nodes.map(node => ({
id: node.id,
data: { label: node.name }
})),
edges: props.graphData.edges.map(edge => ({
source: edge.source_id,
target: edge.target_id,
data: { label: edge.type }
}))
};
graphInstance.setData(formattedData);
graphInstance.render();
};
onMounted(() => {
renderGraph();
window.addEventListener('resize', renderGraph);
});
watch(() => props.graphData, renderGraph, { deep: true });
</script>
<style scoped>
.graph-container {
background: #F7F7F7;
border-radius: 16px;
width: 100%;
height: 600px;
overflow: hidden;
}
</style>

View File

@ -11,7 +11,7 @@
<div class="graph-container layout-container" v-else> <div class="graph-container layout-container" v-else>
<HeaderComponent <HeaderComponent
title="图数据库" title="图数据库"
:description="`${graphInfo?.database_name || ''} - 共 ${graphInfo?.entity_count || 0} 实体,${graphInfo?.relationship_count || 0} 个关系`" :description="`${graphInfo?.database_name || ''} - 共 ${graphInfo?.entity_count || 0} 实体,${graphInfo?.relationship_count || 0} 个关系。向量模型:${graphInfo?.embed_model_name || '未上传文件'}`"
> >
<template #actions> <template #actions>
<div class="status-wrapper"> <div class="status-wrapper">
@ -43,7 +43,7 @@
</div> </div>
</div> </div>
<div class="main" id="container" ref="container" v-show="graphData.nodes.length > 0"></div> <div class="main" id="container" ref="container" v-show="graphData.nodes.length > 0"></div>
<a-empty v-show="graphData.nodes.length === 0" style="padding: 4rem 0;"/> <a-empty v-show="graphData.nodes.length === 0" style="padding: 4rem 0;"/>
<a-modal <a-modal
:open="state.showModal" title="上传文件" :open="state.showModal" title="上传文件"
@ -51,6 +51,11 @@
@cancel="() => state.showModal = false" @cancel="() => state.showModal = false"
ok-text="添加到图数据库" cancel-text="取消" ok-text="添加到图数据库" cancel-text="取消"
:confirm-loading="state.precessing"> :confirm-loading="state.precessing">
<div v-if="graphInfo?.embed_model_name">
<p>当前图数据库向量模型{{ graphInfo?.embed_model_name }}</p>
<p>当前所选择的向量模型是 {{ configStore.config.embed_model }}</p>
</div>
<p v-else>第一次创建之后将无法修改向量模型当前向量模型 {{ configStore.config.embed_model }}</p>
<div class="upload"> <div class="upload">
<a-upload-dragger <a-upload-dragger
class="upload-dragger" class="upload-dragger"
@ -58,7 +63,7 @@
name="file" name="file"
:fileList="fileList" :fileList="fileList"
:max-count="1" :max-count="1"
:disabled="state.precessing" :disabled="state.precessing || (graphInfo?.embed_model_name && graphInfo?.embed_model_name !== configStore.config.embed_model)"
action="/api/data/upload" action="/api/data/upload"
@change="handleFileUpload" @change="handleFileUpload"
@drop="handleDrop" @drop="handleDrop"
@ -181,7 +186,7 @@ const loadSampleNodes = () => {
graphData.nodes = data.result.nodes graphData.nodes = data.result.nodes
graphData.edges = data.result.edges graphData.edges = data.result.edges
console.log(graphData) console.log(graphData)
randerGraph() setTimeout(() => randerGraph(), 500)
}) })
.catch((error) => { .catch((error) => {
message.error(error.message); message.error(error.message);
@ -195,12 +200,6 @@ const onSearch = () => {
return return
} }
const cur_embed_model = configStore.config.embed_model
if (cur_embed_model !== 'zhipu-embedding-3') {
message.error('当前不支持实体检索,请在设置中选择向量模型为 zhipu-embedding-3')
return
}
state.searchLoading = true state.searchLoading = true
fetch(`/api/data/graph/node?entity_name=${state.searchInput}`) fetch(`/api/data/graph/node?entity_name=${state.searchInput}`)
.then((res) => { .then((res) => {

View File

@ -24,7 +24,7 @@
<div class="section"> <div class="section">
<div class="card"> <div class="card">
<span class="label">{{ items?.embed_model.des }}</span> <span class="label">{{ items?.embed_model.des }}</span>
<a-select style="width: 200px" <a-select style="width: 300px"
:value="configStore.config?.embed_model" :value="configStore.config?.embed_model"
@change="handleChange('embed_model', $event)" @change="handleChange('embed_model', $event)"
> >
@ -36,7 +36,7 @@
</div> </div>
<div class="card"> <div class="card">
<span class="label">{{ items?.reranker.des }}</span> <span class="label">{{ items?.reranker.des }}</span>
<a-select style="width: 200px" <a-select style="width: 300px"
:value="configStore.config?.reranker" :value="configStore.config?.reranker"
@change="handleChange('reranker', $event)" @change="handleChange('reranker', $event)"
:disabled="!configStore.config.enable_reranker" :disabled="!configStore.config.enable_reranker"