Commit bd79c9e5 authored by wangteng's avatar wangteng

模型更换

parent bbee8108
......@@ -16,6 +16,8 @@ web/configs/llm.yaml
web/configs/db_defaults.yaml
web/backend/configs/llm.yaml
workflow/config.yaml
backend-deploy.tgz
frontend-deploy.tgz
# ── web 运行时数据 ──
web/data/
......
#!/usr/bin/env python3
"""db-tools 发版:上传 → 备份旧版 → 解压 → 重启 → 验证。
不动 /opt/app/db-tool/web/data/(web.db、backups、backup-config.json、sources)
不动服务器自己的 configs/llm.yaml(tar 里就没有)"""
import sys, os, paramiko
后端发布包包含私有 configs/llm.yaml,部署后无需再配置服务器环境变量。"""
import os
import sys
import paramiko
HOST, USER = "192.168.20.175", "root"
pw = os.environ.get("DBTOOL_PW")
......@@ -26,7 +29,7 @@ sftp = c.open_sftp()
sftp.put("backend-deploy.tgz", "/tmp/dbtool-backend.tgz")
sftp.put("frontend-deploy.tgz", "/tmp/dbtool-frontend.tgz")
sftp.close()
print("uploaded both tarballs\n")
print("uploaded both deployment packages\n")
# 0) 部署前快照(证明 data/ 没被碰)
run("md5sum /opt/app/db-tool/web/data/web.db; ls /opt/app/db-tool/web/data/backups | wc -l")
......@@ -36,10 +39,13 @@ run("TS=$(date +%Y%m%d-%H%M%S) && mkdir -p /opt/app/db-tool/releases/$TS && "
"cp -a /opt/app/db-tool/web/backend /opt/app/db-tool/releases/$TS/backend && "
"cp -a /opt/web/db-tool /opt/app/db-tool/releases/$TS/frontend && echo backup at $TS")
# 2) 解压后端(覆盖 backend/,不动 data/、logs/、服务器 llm.yaml)
# 2) 解压后端(不动 data/、logs/),并锁定包内私有 LLM 配置权限
run("tar xzf /tmp/dbtool-backend.tgz -C /opt/app/db-tool/web/ && "
"find /opt/app/db-tool/web/backend -name __pycache__ -type d -exec rm -rf {} + ; "
"ls /opt/app/db-tool/web/backend/configs/")
"test -f /opt/app/db-tool/web/backend/configs/llm.yaml && "
"chmod 600 /opt/app/db-tool/web/backend/configs/llm.yaml && "
"sed -E -e 's/^(api_key:[[:space:]]*).*/\\1[REDACTED]/' "
"/opt/app/db-tool/web/backend/configs/llm.yaml")
# 3) 前端:清旧 assets 再解压(避免旧 hash 文件堆积)
run("rm -rf /opt/web/db-tool/assets && tar xzf /tmp/dbtool-frontend.tgz -C /opt/web/db-tool/ && ls /opt/web/db-tool/ /opt/web/db-tool/assets/")
......
......@@ -5,29 +5,11 @@
# 真实 llm.yaml 已在 .gitignore,不会上传到仓库。
# ============================================================
# ── 方式 1:MiniMax(推荐本项目用户使用) ──
provider: minimax
api_key: sk-请填入你的_MiniMax_API_Key
base_url: https://api.minimaxi.com/anthropic
model: MiniMax-Text-01
# ── 方式 2:Anthropic Claude ──
# provider: anthropic
# api_key: sk-ant-xxxxx
# model: claude-sonnet-5
# (base_url 留空,用 Anthropic 官方)
# ── 方式 3:OpenAI ──
# provider: openai
# api_key: sk-xxxxx
# model: gpt-4o
# base_url: https://api.openai.com/v1
# ── 方式 4:自部署 / 第三方代理(OpenAI 兼容) ──
# provider: openai
# api_key: 自定义
# base_url: https://your-proxy.com/v1
# model: your-model-name
# ── 公司内部 Anthropic 兼容转换服务 ──
# 推荐在服务环境中设置 ANTHROPIC_AUTH_TOKEN,不要将真实令牌写入仓库。
# api_key: sk-请填入服务令牌
base_url: http://192.168.40.199:8080
model: glm-5.3-flash
# 生成参数(通用)
max_tokens: 1024
......
......@@ -4,7 +4,7 @@
用自然语言解释「这条数据为什么不合规」,并审查「规则描述与实现代码是否一致」。
设计:
- 强制 LLM 输出 JSON(用 complete_json),失败兜底走自然语言
- 强制 LLM 输出 JSON;格式异常时展示模型原文
- JSON 字段定义:
violation_reason: 自然语言违规原因
consistency: 'match' | 'code_bug' | 'rule_ambiguous'
......@@ -23,57 +23,20 @@ logger = get_logger("backend.ai_explain")
_SYSTEM_PROMPT = (
"你是数据质量分析助手。用户的诉求是:当一条数据被判为不合规时,希望从两个角度得到答案——"
"1) 这条数据哪里错了(必须对照规则的【自然语言描述】来回答,方便业务人员理解);"
"2) 规则的【自然语言描述】与【实现代码】是否一致——实现代码可能写错。"
"请用中文思考与回答。先把分析想清楚,再用严格的 JSON 格式输出最终结论。"
"你是数据质量分析助手。直接返回 JSON,不要输出思考过程、Markdown 或额外文字。"
)
_USER_PROMPT_TEMPLATE = """请分析下面这条「被判为不合规」的数据。
_USER_PROMPT_TEMPLATE = """分析这条被判为不合规的数据,并只返回 JSON。
字段:{field_key}({field_comment})
原始值:{value_repr}
规则说明:{rule_desc}
规则实现:{code_block}
【字段】
- 列名(数据库列):{field_key}
- 字段说明(数据库注释,可空):{field_comment}
输出:
{{"violation_reason":"用中文说明具体不合规点,80字以内","consistency":"match|code_bug|rule_ambiguous","consistency_detail":"说明规则描述与实现是否一致,60字以内"}}
【数据】
- 原始值(从数据库读出来的,可能是字符串 / 数字 / NULL,请按真实类型理解):{value_repr}
【规则描述】(自然语言 —— 作者写的约束条件,**业务人员用这个理解规则**)
{rule_desc}
【规则代码】(实现该规则的代码:regex 字符串 或 Python 函数 —— **可能写错**)
{code_block}
请做两件事。
═══ 任务 1:违规原因(violation_reason)═══
- **必须基于「规则描述」(自然语言)回答,不是基于代码** —— 业务人员看不懂代码,要的是用人话讲清楚
- 指出原始值中具体哪个位置 / 子串 / 数值 / 字段违反了「规则描述」里的哪一条要求
- 例:「按规则描述要求,身份证号长度应为 18 位;而这条原始值实际为 19 位(多了一个空格),因此被判为不合规」
- 如果「规则描述」本身写得比较宽松但代码判得很严,那是代码的问题(记到任务 2),这里仍按描述如实回答
- 如果原始值是 NULL / 空,先说明「该值为空」再继续
═══ 任务 2:描述 vs 代码一致性(consistency)═══
- "match":代码严格实现了「规则描述」的全部约束 → 数据违规纯粹是数据本身的问题
- "code_bug":「规则描述」清楚明确,但代码有 bug(漏条件 / 判断反了 / 范围不对 /
边界写错 / 该用 AND 写成 OR 等)→ 在 consistency_detail 里**明确指出代码哪一行 /
哪一段不符合规则描述**,并给出修复建议(最好附修改后的代码片段)
- "rule_ambiguous":代码本身没明显 bug,但「规则描述」有歧义 / 缺失 / 前后矛盾 /
用语模糊 → 在 consistency_detail 里指出歧义点(用户未必能自己看出来)
═══ 输出格式(严格 JSON)═══
- 严格按下面的 JSON 结构输出,**不要** Markdown 代码块(不要 ```json 包裹),**不要** 任何额外文字
- 字符串值里如果需要引号,**必须用中文全角引号「」或转义 \\",绝不能用裸的 ASCII 双引号**(这是历史上最容易踩的坑)
- consistency 只能取上面三个值之一;其他值会被前端视为「未明确」
```json
{{
"violation_reason": "用一段中文自然语言回答任务 1",
"consistency": "match" | "code_bug" | "rule_ambiguous",
"consistency_detail": "针对任务 2 的补充说明(match 时可写"无")"
}}
```"""
若规则说明不足以判断,consistency 使用 rule_ambiguous。"""
# 重试时附加的「修复 JSON」提示 —— 短而直接,避免 LLM 二次跑飞
......@@ -168,11 +131,10 @@ def explain_issue(
note: 失败原因 / 解析回退说明 / 重试说明
attempts: 实际调 LLM 几次(1~3,给前端展示)
2026-08-24 重试策略:JSON 解析失败 → 附带 _JSON_FIX_PROMPT 再调一次,
最多 3 次;超过仍失败 → 降级把原文塞 violation_reason + consistency='unknown',
不抛异常、不阻断弹框。
JSON 解析失败时展示模型原文,不进行额外模型请求,避免分析弹窗长时间等待。
"""
MAX_RETRIES = 3
# 分析弹窗应快速返回。格式异常时回显模型原文,不再发起额外模型请求。
MAX_RETRIES = 1
if rule_type not in RULE_TYPES:
return {
......@@ -222,7 +184,9 @@ def explain_issue(
)
try:
text = client.complete(prompt, system=_SYSTEM_PROMPT, json_mode=True)
text = client.complete(
prompt, system=_SYSTEM_PROMPT, json_mode=True, max_tokens=512
)
except LLMUnavailable as e:
# 不可用(无 key / 网络挂 / 限流)—— 不再重试,立即降级
logger.warning(f"[ai_explain] LLM 调用失败(attempt={attempt}):{e}")
......
......@@ -161,7 +161,7 @@ def gen_regex(desc: str) -> tuple[str, str]:
prompt = _USER_TEMPLATE_FIX.format(
desc=desc, bad_regex=bad_regex, error_msg=error_msg
)
text = client.complete(prompt, system=_SYSTEM_PROMPT)
text = client.complete(prompt, system=_SYSTEM_PROMPT, max_tokens=512)
last_regex = _extract_regex(text)
ok, err = _validate_regex(last_regex)
if ok:
......
......@@ -38,23 +38,16 @@ from web.backend.core.ai_regex import gen_regex, test_regex # noqa: E402,F401
# ── System Prompt ───────────────────────────────────────
_SYSTEM_PROMPT = "你是 Python 程序员,擅长写简短的数据校验函数。"
_DESCRIPTION_SYSTEM_PROMPT = "你是数据质量规则专家,擅长将简短的规则名称整理为准确、可执行的校验说明。"
_DESCRIPTION_SYSTEM_PROMPT = "你是数据质量规则专家,擅长用一句简洁的话说明数据校验规则。"
_DESCRIPTION_TEMPLATE = """请为下面的数据校验规则生成一条详细、可执行且便于业务人员阅读的规则说明。
规则名称:「{name}」
AI 生成提示词(最高优先级):「{prompt}」
现有规则说明(可能为空或不完整):「{desc}」
_DESCRIPTION_TEMPLATE = """用一句中文生成 30-40 字规则说明,只输出说明本身。
规则名称:{name}
优先依据:{prompt}
现有说明:{desc}
规则类型:{rule_type}
仅保留生成规则所需的关键格式或范围,不要补充额外校验细节。"""
要求:
1. 只输出规则说明本身,不要标题、序号、引号或 Markdown
2. 优先严格依据 AI 生成提示词,写清楚数据需要满足的格式、长度、字符范围、取值范围、日期范围、枚举值或校验关系
3. 对身份证、手机号、统一社会信用代码、银行卡号等常见证件/号码,名称或提示词已经明确其类型时必须写出标准的基础结构。例如中国大陆居民身份证至少说明:共 18 位、前 17 位为数字、最后一位为数字或 X/x;手机号至少说明 11 位数字及号码段要求(若提示词指定)。
4. 行政区划码、出生日期、校验码等额外校验,只有提示词明确要求时才加入;不要编造提示词中没有的业务阈值、枚举值或校验算法。说明中的每一项约束都必须能够由随后生成的规则实现
5. 使用中文,建议 80 到 250 字;信息较少时可简短,但不能只重复规则名称
6. 这段说明会直接展示在校验结果列表中,应使业务人员无需查看正则或代码也能判断不合规原因
"""
_MAX_AI_DESCRIPTION_LENGTH = 40
# ── User Prompt ─────────────────────────────────────────
......@@ -227,6 +220,7 @@ def gen_rule_description(name: str, desc: str, rule_type: str, prompt: str = "")
rule_type=rule_type,
),
system=_DESCRIPTION_SYSTEM_PROMPT,
max_tokens=512,
)
except LLMUnavailable as e:
logger.warning(f"AI 规则说明生成失败 name={name!r} err={e}")
......@@ -238,7 +232,9 @@ def gen_rule_description(name: str, desc: str, rule_type: str, prompt: str = "")
description = description.replace("\n", " ").strip().strip('"')
if not description:
return "", "AI 未生成规则说明"
return description[:500], "已生成详细规则说明"
# 兜底截断,避免模型未遵守长度要求时撑大规则列表。
description = description[:_MAX_AI_DESCRIPTION_LENGTH].rstrip(",、;:。 ")
return description, "已生成简短规则说明(30-40字)"
# ── 主入口 ─────────────────────────────────────────────
......@@ -287,7 +283,7 @@ def gen_rule(desc: str, rule_type: str) -> tuple[str, str]:
prompt = tpl_fix.format(
desc=desc, bad_code=bad_code, error_msg=error_msg
)
text = client.complete(prompt, system=_SYSTEM_PROMPT)
text = client.complete(prompt, system=_SYSTEM_PROMPT, max_tokens=512)
last_code = _extract_code(text)
ok, err = validate_user_code(last_code)
if ok:
......
This diff is collapsed.
......@@ -7,9 +7,8 @@ pymysql>=1.1
oracledb>=1.4
dmPython>=2.5
# LLM(web/backend/core/ai_regex.py → core/llm.py 用)
# anthropic 走 Anthropic SDK(MiniMax 也走这个,兼容接口);openai 走 OpenAI SDK
# anthropic 走 Anthropic SDK(公司内部服务兼容该接口)
# pyyaml 读 web/backend/configs/llm.yaml
anthropic>=0.30
openai>=1.30
pyyaml>=6.0
cryptography>=42.0
......@@ -20,6 +20,7 @@
from __future__ import annotations
import asyncio
import time
import re
from typing import Any, Optional
......@@ -66,10 +67,17 @@ async def ai_gen_rule(req: GenRuleRequest):
logger.info(f"POST /api/ai/rule rule_type={req.rule_type!r} desc={req.desc!r}")
started = time.monotonic()
try:
description, desc_note = gen_rule_description(req.name, req.desc, req.rule_type, req.prompt)
# 两项输出都只依赖用户的原始需求,可以并行请求,避免用户等待两次模型响应。
source_desc = (req.prompt or req.desc or req.name).strip()
description_task = asyncio.to_thread(
gen_rule_description, req.name, req.desc, req.rule_type, req.prompt
)
code_task = asyncio.to_thread(gen_rule, source_desc, req.rule_type)
(description, desc_note), (code, note) = await asyncio.gather(
description_task, code_task
)
if not description:
return GenRuleResponse(ok=False, code="", desc="", note=desc_note)
code, note = gen_rule(description, req.rule_type)
except Exception as e:
elapsed_ms = (time.monotonic() - started) * 1000
logger.exception(f"❌ AI 规则服务抛出异常(耗时 {elapsed_ms:.0f}ms)")
......
......@@ -41,7 +41,7 @@
<el-form-item label="规则名称" required><el-input v-model="form.name" placeholder="例如:身份证格式校验" /></el-form-item>
<el-form-item label="规则类型"><el-radio-group v-model="form.rule_type"><el-radio-button label="regex">正则</el-radio-button><el-radio-button label="number">数值</el-radio-button><el-radio-button label="date">日期</el-radio-button><el-radio-button label="string">字符串</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="AI生成提示词"><el-input v-model="form.ai_prompt" type="textarea" :rows="3" maxlength="2000" show-word-limit placeholder="例如:校验中国大陆居民身份证。要求为 18 位,前 17 位为数字,末位为数字或 X;同时校验行政区划码、出生日期和校验码。" /></el-form-item>
<el-form-item label="规则说明" required><el-input v-model="form.description" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="AI 将根据提示词生成详细说明;也可以手动补充或修改" /></el-form-item>
<el-form-item label="规则说明" required><el-input v-model="form.description" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="AI 将生成 30-40 字简短说明;也可以手动补充或修改" /></el-form-item>
<el-form-item :label="form.rule_type === 'regex' ? '正则表达式' : '校验代码'" required>
<el-input v-model="ruleContent" type="textarea" :rows="form.rule_type === 'regex' ? 2 : 8" :placeholder="form.rule_type === 'regex' ? '^.+$' : 'def check(value):\n return True'" />
<div class="expression-actions">
......@@ -98,8 +98,8 @@ async function generateByAI() {
if (!result.desc || !result.desc.trim()) throw new Error('AI 未返回规则说明,请确认后端已更新并重试')
ruleContent.value = result.code
form.description = result.desc
aiNote.value = result.note || 'AI 已根据提示词生成详细规则说明和表达式'
ElMessage.success('AI 已生成详细规则说明和表达式')
aiNote.value = result.note || 'AI 已生成简短规则说明和表达式'
ElMessage.success('AI 已生成简短规则说明和表达式')
} catch (e) { aiNote.value = e.message; ElMessage.error(`AI 生成失败:${e.message}`) } finally { aiLoading.value = false }
}
async function validateExpression() {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment