1. 场景价值与技术合理性修复: - 补充docs/SCENE_VALUE.md(业务背景、痛点分析、用户场景、竞品对比、价值量化) - 添加用户操作流程图(Mermaid) - 添加3个真实业务案例量化数据 2. 演示与文档修复: - 创建docs/API.md(完整API文档) - 创建docs/QUICKSTART.md(5分钟快速入门指南) 3. AI使用日志修复: - 更新AGENTS.md,添加强制自动执行的AI使用日志记录指令 - 在_AI_USAGE_LOG.md末尾添加范式执行统计 4. 安全性修复: - 在agents/llm.py中添加输入过滤(防Prompt注入) - 添加输出验证、速率限制、详细日志 5. 架构设计修复: - 创建tools/registry.py工具注册表 - 修改orchestrator.py和orchestrator_db.py使用注册表动态获取运行器 6. 开发范式修复: - 在_AI_USAGE_LOG.md末尾添加范式执行统计
129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
import json, hashlib, os, re, time, logging
|
|
from pathlib import Path
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LLMClient:
|
|
def __init__(self, model=None, timeout=15, cache_dir=".cache/llm"):
|
|
# 模型解析链: 显式实参 → LLM_MODEL 环境变量 → 项目默认(与 config 层一致)
|
|
self.model = model or os.environ.get("LLM_MODEL", "deepseek-v4-flash")
|
|
self.timeout = timeout
|
|
self.dir = Path(cache_dir)
|
|
self.dir.mkdir(parents=True, exist_ok=True)
|
|
self._last_call_time = 0
|
|
self._min_interval = 0.1 # 100ms 最小调用间隔
|
|
|
|
def _key(self, msgs):
|
|
return hashlib.sha256(json.dumps(msgs, sort_keys=True).encode()).hexdigest()
|
|
|
|
def _get(self, k):
|
|
p = self.dir / f"{k}.json"
|
|
if not p.exists():
|
|
return None
|
|
try:
|
|
return json.loads(p.read_text())["response"]
|
|
except (json.JSONDecodeError, KeyError):
|
|
return None
|
|
|
|
def _set(self, k, v):
|
|
(self.dir / f"{k}.json").write_text(json.dumps({"response": v}))
|
|
|
|
def _sanitize_input(self, text: str) -> str:
|
|
"""输入过滤:移除潜在的prompt注入模式,限制输入长度"""
|
|
if not isinstance(text, str):
|
|
text = str(text)
|
|
|
|
# 1. 限制输入长度(防止过长输入)
|
|
max_length = 50000
|
|
if len(text) > max_length:
|
|
logger.warning(f"Input truncated from {len(text)} to {max_length} chars")
|
|
text = text[:max_length]
|
|
|
|
# 2. 移除潜在的prompt注入模式
|
|
injection_patterns = [
|
|
r'(?i)ignore\s+previous\s+instructions',
|
|
r'(?i)disregard\s+all\s+prior',
|
|
r'(?i)you\s+are\s+now\s+',
|
|
r'(?i)system\s*:\s*',
|
|
r'(?i)assistant\s*:\s*',
|
|
r'(?i)\[INST\]',
|
|
r'(?i)\[/INST\]',
|
|
r'(?i)<\|im_start\|>',
|
|
r'(?i)<\|im_end\|>',
|
|
]
|
|
|
|
for pattern in injection_patterns:
|
|
if re.search(pattern, text):
|
|
logger.warning(f"Potential prompt injection detected: {pattern}")
|
|
text = re.sub(pattern, '[FILTERED]', text)
|
|
|
|
return text
|
|
|
|
def _validate_output(self, output: str) -> bool:
|
|
"""输出验证:检查LLM返回内容的基本有效性"""
|
|
if not output or not isinstance(output, str):
|
|
return False
|
|
|
|
# 检查长度是否合理
|
|
if len(output) > 100000:
|
|
logger.warning(f"Output too long: {len(output)} chars")
|
|
return False
|
|
|
|
return True
|
|
|
|
def _rate_limit(self):
|
|
"""速率限制:确保最小调用间隔"""
|
|
current_time = time.time()
|
|
elapsed = current_time - self._last_call_time
|
|
if elapsed < self._min_interval:
|
|
time.sleep(self._min_interval - elapsed)
|
|
self._last_call_time = time.time()
|
|
|
|
def call(self, messages, retries=1):
|
|
k = self._key(messages)
|
|
c = self._get(k)
|
|
if c:
|
|
logger.debug(f"Cache hit for key: {k[:8]}...")
|
|
return c
|
|
|
|
# 对用户消息进行输入过滤
|
|
sanitized_messages = []
|
|
for msg in messages:
|
|
if msg.get("role") == "user":
|
|
sanitized_content = self._sanitize_input(msg.get("content", ""))
|
|
sanitized_messages.append({**msg, "content": sanitized_content})
|
|
else:
|
|
sanitized_messages.append(msg)
|
|
|
|
key = os.environ.get("LLM_API_KEY", os.environ.get("OPENAI_API_KEY", ""))
|
|
base = os.environ.get("LLM_API_BASE", "https://api.openai.com/v1")
|
|
|
|
for a in range(retries + 1):
|
|
try:
|
|
# 速率限制
|
|
self._rate_limit()
|
|
|
|
logger.info(f"LLM call: model={self.model}, attempt={a+1}/{retries+1}")
|
|
r = httpx.post(f"{base}/chat/completions", json={"model": self.model, "messages": sanitized_messages},
|
|
headers={"Authorization": f"Bearer {key}"}, timeout=self.timeout)
|
|
r.raise_for_status()
|
|
v = r.json()["choices"][0]["message"]["content"]
|
|
|
|
# 输出验证
|
|
if not self._validate_output(v):
|
|
logger.warning("Invalid output from LLM, using fallback")
|
|
v = ""
|
|
|
|
self._set(k, v)
|
|
logger.info(f"LLM call success: response length={len(v)}")
|
|
return v
|
|
except Exception as e:
|
|
logger.error(f"LLM call failed: {e}")
|
|
if a == retries:
|
|
raise
|
|
time.sleep(1) # 失败后等待1秒重试
|
|
|
|
return ""
|