Files
rag/core/llm_utils.py
lacerate551 279e2bf47c chore: 配置集中化、LLM 参数调整与 gitignore 更新
- config.example: 新增 MINERU_PREFER_V2、标题规则引擎、表单二次校正等配置项
- document_routes: DEV_MODE 判断统一收归 config.py
- llm_utils: quick_yes_no max_tokens 10→128,避免截断过短回答
- knowledge/router: 路由 LLM 调用 max_tokens 100→512
- feedback: 反馈分析 LLM 调用 max_tokens 200→512
- .gitignore: 新增 scripts/ 和 plans/ 目录忽略规则

🤖 Generated with [Qoder][https://qoder.com]
2026-06-08 16:14:17 +08:00

361 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
LLM 调用工具函数
统一封装 LLM 调用模式,减少代码重复。
"""
import json
import re
import logging
from typing import List, Optional, Union, Iterator, Callable
logger = logging.getLogger(__name__)
def call_llm(
client,
prompt: str,
model: str,
temperature: float = 0.3,
max_tokens: int = 1000,
messages: List[dict] = None,
stream: bool = False,
**kwargs
) -> Union[str, Iterator, None]:
"""
统一的 LLM 调用封装
Args:
client: OpenAI 客户端实例
prompt: 用户提示(当 messages 为 None 时使用)
model: 模型名称
temperature: 温度参数 (0-1)
max_tokens: 最大 token 数
messages: 完整消息列表(优先于 prompt
stream: 是否启用流式输出
**kwargs: 其他参数(如 response_format, tools 等)
Returns:
流式模式返回 stream 迭代器,否则返回响应内容字符串
调用失败返回 None
Example:
# 简单调用
response = call_llm(client, "你好", model, temperature=0.3)
# 使用消息列表
response = call_llm(client, "", model, messages=[
{"role": "system", "content": "你是助手"},
{"role": "user", "content": "你好"}
])
# 流式调用
for chunk in call_llm(client, prompt, model, stream=True):
if chunk.choices and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
"""
if messages is None:
messages = [{"role": "user", "content": prompt}]
try:
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
stream=stream,
**kwargs
)
if stream:
return response
content = response.choices[0].message.content
# 推理模型兼容content 为空时尝试从 reasoning_content 提取
if not content or not content.strip():
reasoning = getattr(response.choices[0].message, 'reasoning_content', None)
if reasoning and reasoning.strip():
# 从思维链中提取 JSON 块作为内容
json_match = re.search(r'\{[\s\S]*\}', reasoning)
if json_match:
logger.info("LLM: content为空从reasoning_content提取JSON")
return json_match.group().strip()
logger.warning("LLM 返回空 content可能需要增大 max_tokens")
return None
return content.strip()
except Exception as e:
logger.warning(f"LLM 调用失败: {e}")
return None
def call_llm_stream(
client,
prompt: str,
model: str,
temperature: float = 0.3,
max_tokens: int = 1000,
messages: List[dict] = None,
error_prefix: str = "[错误]",
**kwargs
) -> Iterator[str]:
"""
流式 LLM 调用(生成器封装)
自动处理流式响应,逐块 yield 文本内容。
Args:
client: OpenAI 客户端实例
prompt: 用户提示
model: 模型名称
temperature: 温度参数
max_tokens: 最大 token 数
messages: 完整消息列表
error_prefix: 错误时的前缀
**kwargs: 其他参数
Yields:
响应文本片段
Example:
for text in call_llm_stream(client, "讲个故事", model):
print(text, end="", flush=True)
"""
if messages is None:
messages = [{"role": "user", "content": prompt}]
try:
stream = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
stream=True,
**kwargs
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
except Exception as e:
logger.error(f"LLM 流式调用失败: {e}")
yield f"{error_prefix} 调用大模型失败: {str(e)}"
def call_llm_with_retry(
client,
prompt: str,
model: str,
max_retries: int = 3,
retry_delay: float = 1.0,
**kwargs
) -> Optional[str]:
"""
带重试的 LLM 调用
Args:
client: OpenAI 客户端实例
prompt: 用户提示
model: 模型名称
max_retries: 最大重试次数
retry_delay: 重试间隔(秒)
**kwargs: 传递给 call_llm 的其他参数
Returns:
响应内容,全部失败返回 None
"""
import time
for attempt in range(max_retries):
result = call_llm(client, prompt, model, **kwargs)
if result is not None:
return result
if attempt < max_retries - 1:
logger.debug(f"LLM 调用重试 {attempt + 1}/{max_retries}")
time.sleep(retry_delay)
logger.warning(f"LLM 调用失败,已重试 {max_retries}")
return None
def parse_json_from_response(content: str) -> Optional[dict]:
"""
从 LLM 响应中解析 JSON
自动处理 ```json 或 ``` 代码块格式
Args:
content: LLM 返回的原始内容
Returns:
解析后的字典,失败返回 None
"""
if not content:
return None
# 提取 JSON 代码块(支持 ```json 或 ```
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', content)
json_str = json_match.group(1) if json_match else content
try:
return json.loads(json_str.strip())
except (json.JSONDecodeError, TypeError, ValueError):
return None
def parse_json_list_from_response(content: str) -> Optional[List[dict]]:
"""
从 LLM 响应中解析 JSON 数组
Args:
content: LLM 返回的原始内容
Returns:
解析后的列表,失败返回 None
"""
if not content:
return None
# 提取 JSON 代码块
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', content)
json_str = json_match.group(1) if json_match else content
try:
result = json.loads(json_str.strip())
if isinstance(result, list):
return result
# 如果是 {"items": [...]} 格式,提取 items
if isinstance(result, dict):
for key in ['items', 'data', 'results', 'questions']:
if key in result and isinstance(result[key], list):
return result[key]
return None
except (json.JSONDecodeError, TypeError, ValueError):
return None
def extract_json_object(content: str) -> Optional[dict]:
"""
多策略从 LLM 响应中提取 JSON 对象(增强版)
在 parse_json_from_response 基础上增加 fallback 策略:
1. 先调用 parse_json_from_responsemarkdown 代码块 → 直接解析)
2. 失败后 fallback 到正则匹配最外层 {...} 块
Args:
content: LLM 返回的原始内容
Returns:
解析后的字典,全部策略失败返回 None
"""
if not content:
return None
# 策略1+2markdown 代码块提取 + 直接 json.loads
result = parse_json_from_response(content)
if result is not None and isinstance(result, dict):
return result
# 策略3fallback正则匹配最外层 JSON 对象 {...}
brace_match = re.search(r'\{[\s\S]*\}', content)
if brace_match:
try:
parsed = json.loads(brace_match.group(0))
if isinstance(parsed, dict):
return parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass
return None
def extract_json_list(content: str) -> Optional[list]:
"""
多策略从 LLM 响应中提取 JSON 数组(增强版)
在 parse_json_list_from_response 基础上增加 fallback 策略:
1. 先调用 parse_json_list_from_responsemarkdown 代码块 → 直接解析 → 嵌套提取)
2. 失败后 fallback 到正则匹配最外层 [...] 块
Args:
content: LLM 返回的原始内容
Returns:
解析后的列表,全部策略失败返回 None
"""
if not content:
return None
# 策略1+2markdown 代码块提取 + 直接 json.loads + 嵌套 key 提取
result = parse_json_list_from_response(content)
if result is not None:
return result
# 策略3fallback正则匹配最外层 JSON 数组 [...]
bracket_match = re.search(r'\[[\s\S]*\]', content)
if bracket_match:
try:
parsed = json.loads(bracket_match.group(0))
if isinstance(parsed, list):
return parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass
return None
# ==================== 便捷函数 ====================
def quick_ask(
client,
prompt: str,
model: str,
temperature: float = 0.3
) -> str:
"""
快速提问(简化版)
Args:
client: OpenAI 客户端
prompt: 问题
model: 模型
temperature: 温度
Returns:
回答内容,失败返回空字符串
"""
result = call_llm(client, prompt, model, temperature=temperature)
return result or ""
def quick_yes_no(
client,
prompt: str,
model: str,
keywords: List[str] = None
) -> bool:
"""
快速是/否判断
Args:
client: OpenAI 客户端
prompt: 问题(需包含判断标准)
model: 模型
keywords: 判断为 True 的关键词(默认 ["", "需要", "yes", "true"]
Returns:
布尔判断结果
"""
if keywords is None:
keywords = ["", "需要", "yes", "true"]
result = call_llm(client, prompt, model, temperature=0, max_tokens=128)
if result is None:
return False
result_lower = result.lower()
return any(kw.lower() in result_lower for kw in keywords)