完成和大模型的通信

This commit is contained in:
wandering
2026-06-04 09:58:45 +08:00
parent 37199970c9
commit 580fe4ad6e
13 changed files with 2117 additions and 1820 deletions

View File

@@ -5,20 +5,26 @@ from pathlib import Path
CONFIG_PATH = Path(__file__).parent.parent / "config.json"
DEFAULT_CONFIG: dict[str, str] = {
DEFAULT_CONFIG: dict = {
"language": "Chinese",
"raw_dir": "raw",
"wiki_dir": "wiki",
}
DEFAULT_LLM_CONFIG: dict = {
"model": "qwen/qwen3.6-27b",
"api_base": "http://100.123.83.113:1234/v1",
"api_key": "123456",
}
def load_config() -> dict[str, str]:
def load_config() -> dict:
"""
加载项目配置。
返回
-------
dict[str, str]
dict
配置字典,包含 language、raw_dir、wiki_dir 等键。
"""
if CONFIG_PATH.is_file():
@@ -38,3 +44,19 @@ def get_language() -> str | None:
if language.lower() == "auto":
return None
return language
def get_llm_config() -> dict[str, str]:
"""
获取 LLM 配置。
返回
-------
dict[str, str]
包含 model, api_base, api_key 的字典。
"""
config = load_config()
llm_raw = config.get("llm", {})
# 过滤空值,避免空字符串覆盖默认配置
llm_filtered = {k: v for k, v in llm_raw.items() if v}
return {**DEFAULT_LLM_CONFIG, **llm_filtered}

86
src/llm.py Normal file
View File

@@ -0,0 +1,86 @@
"""LLM client wrapper."""
import logging
from prompt import build_analysis_prompt
logger = logging.getLogger(__name__)
def _create_llm():
"""根据配置文件创建 LLM 实例。"""
from config import get_llm_config
from llama_index.llms.openai_like import OpenAILike
llm_config = get_llm_config()
return OpenAILike(
model=llm_config["model"],
api_base=llm_config["api_base"],
api_key=llm_config.get("api_key", "lm-studio"),
temperature=0.1,
max_tokens=128000,
request_timeout=300.0,
is_chat_model=True,
)
def query_analysis(
purpose: str,
index: str,
source_content: str = "",
configured_language: str | None = None,
) -> str:
"""
构建分析提示并发送给 LLM返回分析报告。
参数
----------
purpose : str
wiki/purpose.md 的内容。
index : str
wiki/index.md 的内容。
source_content : str
源文档文本。
configured_language : str | None
显式覆盖语言设置。
返回
-------
str
LLM 返回的分析报告。
"""
from config import get_llm_config
from llama_index.core.llms import ChatMessage
system_prompt = build_analysis_prompt(
purpose,
index,
source_content=source_content,
configured_language=configured_language,
)
messages = [
ChatMessage(role="system", content=system_prompt),
ChatMessage(
role="user",
content=f"Analyze this source document:\n\n---\n\n{source_content}",
),
]
llm_config = get_llm_config()
llm = _create_llm()
logger.info("开始请求 LLM (model=%s, base=%s)", llm_config["model"], llm_config["api_base"])
try:
parts = []
for resp in llm.stream_chat(messages):
delta = resp.delta
if delta:
parts.append(delta)
text = "".join(parts)
logger.info("LLM 请求完成,响应总长度: %d 字符", len(text))
return text
except Exception as e:
logger.error("LLM 请求失败: %s", e)
raise

View File

@@ -1,115 +1,93 @@
# ---------------------------------------------------------------------------
# 辅助函数
# ---------------------------------------------------------------------------
"""
Unified entry point.
Re-exports from sub-modules for backward compatibility.
"""
import logging
from prompt import build_analysis_prompt, language_rule
from llm import query_analysis
__all__ = ["language_rule", "build_analysis_prompt", "query_analysis"]
logger = logging.getLogger(__name__)
def language_rule(source_content: str, configured_language: str | None = None) -> str:
"""
生成语言规则提示。
def _setup_logging() -> None:
"""配置日志格式和级别,同时输出到终端和文件。"""
from pathlib import Path
参数
----------
source_content : str
用于语言检测回退的源文档文本。
configured_language : str | None
明确配置的输出语言(如 "Chinese""English""auto")。
传入 ``None`` 或 ``"auto"`` 以自动检测。
log_path = Path(__file__).parent.parent / "llmwiki.log"
返回
-------
str
语言规则提示字符串。
"""
if configured_language and configured_language.lower() != "auto":
return f"使用 {configured_language} 语言输出分析报告。"
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# 简单的语言检测回退
if source_content and any("\u4e00" <= char <= "\u9fff" for char in source_content[:500]):
return "源文档包含中文内容,使用简体中文输出分析报告。"
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setFormatter(formatter)
return "使用与源文档相同的语言输出分析报告。"
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[file_handler, stream_handler],
)
# ---------------------------------------------------------------------------
# 主函数build_analysis_prompt
# ---------------------------------------------------------------------------
def main() -> None:
"""执行入口:遍历 raw 目录,对每个源文件进行分析。"""
import sys
from pathlib import Path
_setup_logging()
from config import load_config
config = load_config()
raw_dir = Path(config.get("raw_dir", "raw"))
wiki_dir = Path(config.get("wiki_dir", "wiki"))
# 读取 wiki 上下文
purpose_path = wiki_dir / "purpose.md"
index_path = wiki_dir / "index.md"
purpose = purpose_path.read_text(encoding="utf-8") if purpose_path.exists() else ""
index = index_path.read_text(encoding="utf-8") if index_path.exists() else ""
logger.info("purpose.md: %s", "已加载" if purpose else "未找到")
logger.info("index.md: %s", "已加载" if index else "未找到")
# 收集 raw 目录下的 .md 文件
md_files = sorted(raw_dir.rglob("*.md"))
if not md_files:
logger.info("未找到 %s 下的 .md 文件", raw_dir)
sys.exit(0)
logger.info("共发现 %d 个源文件", len(md_files))
for fp in md_files:
source_content = fp.read_text(encoding="utf-8")
logger.info("=" * 60)
logger.info("分析: %s", fp.relative_to(raw_dir.parent))
logger.info("=" * 60)
try:
result = query_analysis(
purpose=purpose,
index=index,
source_content=source_content,
)
logger.info("分析完成,结果长度: %d 字符", len(result))
logger.info("结果: %s", result)
except Exception as e:
logger.error("分析失败: %s", e, exc_info=True)
sys.exit(1)
def build_analysis_prompt(
purpose: str,
index: str,
source_content: str = "",
configured_language: str | None = None,
) -> str:
"""
第一步提示AI 阅读源文档并生成结构化分析报告。
这是「讨论」步骤——AI 在撰写 wiki 页面之前先对源文档进行推理。
参数
----------
purpose : str
wiki/purpose.md 的内容(可能为空)。
index : str
wiki/index.md 的内容(可能为空)。
source_content : str
用于语言检测回退的源文档文本。
configured_language : str | None
显式覆盖语言设置。传入 ``None`` 时从配置文件读取。
返回
-------
str
完整的系统提示字符串。
"""
from config import get_language
if configured_language is None:
configured_language = get_language()
parts: list[str] = [
"你是一位专业研究分析师。阅读源文档并产出一份结构化的分析报告。",
"不要输出思维链、隐藏推理或思考过程。在内部进行推理,只撰写简洁的最终分析。",
"",
language_rule(source_content, configured_language),
"",
"你的分析需要包含:",
"",
"## 关键实体",
"列出文档中提到的人物、组织、产品、数据集、工具、芯片、算法等。对于每一项:",
"- 名称和类型",
"- 在源文档中的角色(核心还是边缘)",
"- 是否可能已存在于 wiki 中(查阅索引)",
"",
"## 关键概念",
"列出理论、方法、技术、现象。对于每一项:",
"- 名称和简要定义",
"- 为什么在源文档中重要",
"- 是否可能已存在于 wiki 中",
"",
"## 主要论点与发现",
"- 核心主张或结果是什么?",
"- 有什么证据支持这些主张?",
"- 证据的强度如何?",
"",
"## 与现有 Wiki 的关联",
"- 源文档与哪些现有页面相关?",
"- 它是否强化、挑战或扩展了现有知识?",
"",
"## 矛盾与张力",
"- 源文档中是否有任何内容与现有 wiki 内容冲突?",
"- 是否存在内部矛盾或注意事项?",
"",
"## 建议",
"- 应该创建或更新哪些 wiki 页面?",
"- 应该强调什么,弱化什么?",
"- 是否有任何需要向用户指出的开放性问题?",
"",
"分析要全面但简洁,专注于真正重要的内容。",
"",
"如果提供了文件夹上下文,请将其作为分类的提示——文件夹结构通常反映了用户的组织意图(例如,'papers/energy' 表示该文件是与能源相关的论文)。",
"",
f"## Wiki 目的(供参考)\n{purpose}" if purpose else "",
(f"## 当前 Wiki 索引(用于检查现有内容)\n{index}" if index else ""),
]
# 过滤空字符串并用换行符连接
return "\n".join(part for part in parts if part)
if __name__ == "__main__":
main()

108
src/prompt.py Normal file
View File

@@ -0,0 +1,108 @@
"""Analysis prompt builder."""
def language_rule(source_content: str, configured_language: str | None = None) -> str:
"""
生成语言规则提示。
参数
----------
source_content : str
用于语言检测回退的源文档文本。
configured_language : str | None
明确配置的输出语言(如 "Chinese""English""auto")。
传入 ``None`` 或 ``"auto"`` 以自动检测。
返回
-------
str
语言规则提示字符串。
"""
if configured_language and configured_language.lower() != "auto":
return f"使用 {configured_language} 语言输出分析报告。"
# 简单的语言检测回退
if source_content and any("\u4e00" <= char <= "\u9fff" for char in source_content[:500]):
return "源文档包含中文内容,使用简体中文输出分析报告。"
return "使用与源文档相同的语言输出分析报告。"
def build_analysis_prompt(
purpose: str,
index: str,
source_content: str = "",
configured_language: str | None = None,
) -> str:
"""
第一步提示AI 阅读源文档并生成结构化分析报告。
这是「讨论」步骤——AI 在撰写 wiki 页面之前先对源文档进行推理。
参数
----------
purpose : str
wiki/purpose.md 的内容(可能为空)。
index : str
wiki/index.md 的内容(可能为空)。
source_content : str
用于语言检测回退的源文档文本。
configured_language : str | None
显式覆盖语言设置。传入 ``None`` 时从配置文件读取。
返回
-------
str
完整的系统提示字符串。
"""
from config import get_language
if configured_language is None:
configured_language = get_language()
parts: list[str] = [
"你是一位专业研究分析师。阅读源文档并产出一份结构化的分析报告。",
"不要输出思维链、隐藏推理或思考过程。在内部进行推理,只撰写简洁的最终分析。",
"",
language_rule(source_content, configured_language),
"",
"你的分析需要包含:",
"",
"## 关键实体",
"列出文档中提到的人物、组织、产品、数据集、工具、芯片、算法等。对于每一项:",
"- 名称和类型",
"- 在源文档中的角色(核心还是边缘)",
"- 是否可能已存在于 wiki 中(查阅索引)",
"",
"## 关键概念",
"列出理论、方法、技术、现象。对于每一项:",
"- 名称和简要定义",
"- 为什么在源文档中重要",
"- 是否可能已存在于 wiki 中",
"",
"## 主要论点与发现",
"- 核心主张或结果是什么?",
"- 有什么证据支持这些主张?",
"- 证据的强度如何?",
"",
"## 与现有 Wiki 的关联",
"- 源文档与哪些现有页面相关?",
"- 它是否强化、挑战或扩展了现有知识?",
"",
"## 矛盾与张力",
"- 源文档中是否有任何内容与现有 wiki 内容冲突?",
"- 是否存在内部矛盾或注意事项?",
"",
"## 建议",
"- 应该创建或更新哪些 wiki 页面?",
"- 应该强调什么,弱化什么?",
"- 是否有任何需要向用户指出的开放性问题?",
"",
"分析要全面但简洁,专注于真正重要的内容。",
"",
"如果提供了文件夹上下文,请将其作为分类的提示——文件夹结构通常反映了用户的组织意图(例如,'papers/energy' 表示该文件是与能源相关的论文)。",
"",
f"## Wiki 目的(供参考)\n{purpose}" if purpose else "",
(f"## 当前 Wiki 索引(用于检查现有内容)\n{index}" if index else ""),
]
# 过滤空字符串并用换行符连接
return "\n".join(part for part in parts if part)