Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
db-tools
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
yzy
db-tools
Commits
bd79c9e5
Commit
bd79c9e5
authored
Sep 24, 2026
by
wangteng
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
模型更换
parent
bbee8108
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
142 additions
and
240 deletions
+142
-240
.gitignore
.gitignore
+2
-0
deploy_server.py
deploy_server.py
+11
-5
web/backend/configs/llm.example.yaml
web/backend/configs/llm.example.yaml
+6
-24
web/backend/core/ai_explain.py
web/backend/core/ai_explain.py
+17
-53
web/backend/core/ai_regex.py
web/backend/core/ai_regex.py
+2
-2
web/backend/core/ai_rule.py
web/backend/core/ai_rule.py
+12
-16
web/backend/core/llm.py
web/backend/core/llm.py
+78
-133
web/backend/requirements.txt
web/backend/requirements.txt
+1
-2
web/backend/routers/ai.py
web/backend/routers/ai.py
+10
-2
web/src/views/RuleLibraryView.vue
web/src/views/RuleLibraryView.vue
+3
-3
No files found.
.gitignore
View file @
bd79c9e5
...
...
@@ -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/
...
...
deploy_server.py
View file @
bd79c9e5
#!/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
tarball
s
\
n
"
)
print
(
"uploaded both
deployment package
s
\
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/"
)
...
...
web/backend/configs/llm.example.yaml
View file @
bd79c9e5
...
...
@@ -5,31 +5,13 @@
# 真实 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
temperature
:
0.2
timeout
:
30
\ No newline at end of file
timeout
:
30
web/backend/core/ai_explain.py
View file @
bd79c9e5
...
...
@@ -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
}
")
...
...
@@ -288,4 +252,4 @@ def _diagnose_json_failure(text: str) -> str:
bad_quote
=
re
.
search
(
r':\
s*
"[^"]*"\
s*
"'
,
s
)
if
bad_quote
:
return
"疑似 value 里有未转义的 ASCII 双引号"
return
"JSON 结构不合法(具体行号见日志)"
\ No newline at end of file
return
"JSON 结构不合法(具体行号见日志)"
web/backend/core/ai_regex.py
View file @
bd79c9e5
...
...
@@ -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
:
...
...
@@ -178,4 +178,4 @@ def gen_regex(desc: str) -> tuple[str, str]:
logger
.
warning
(
f"AI 正则:LLM 第
{
attempt
}
次调用失败 desc=
{
desc
!
r
}
err=
{
e
}
"
)
return
""
,
f"LLM 调用失败:
{
e
}
"
return
""
,
f"LLM 重试
{
MAX_TRIES
}
次仍未生成合法正则(最后错误:
{
error_msg
}
),请补充描述或手动填写"
\ No newline at end of file
return
""
,
f"LLM 重试
{
MAX_TRIES
}
次仍未生成合法正则(最后错误:
{
error_msg
}
),请补充描述或手动填写"
web/backend/core/ai_rule.py
View file @
bd79c9e5
...
...
@@ -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
:
...
...
web/backend/core/llm.py
View file @
bd79c9e5
"""LLM 客户端封装(
支持 MiniMax / Anthropic / OpenAI
)
"""LLM 客户端封装(
公司内部 Anthropic 兼容协议
)
直接拷贝自 web/core/llm.py,2026-08-20 接入 web 用作「AI 生成正则」。
改动点:default_config_path() 改成读 web/backend/configs/llm.yaml,其他不动,
...
...
@@ -7,13 +7,9 @@
所有方法都设计为可降级 — 如果 LLM 不可用(无 API Key / 超时 / 解析失败),返回 None 或 fallback 值,
不阻断主流程。
支持的 Provider:
- minimax: MiniMax(M2.7 / MiniMax-Text-01 等)— 通过 Anthropic 兼容接口
- anthropic: Claude(官方 Anthropic SDK)
- openai: GPT(OpenAI 兼容协议)
配置优先级(从高到低):
1. 环境变量(最高):LLM_PROVIDER / LLM_API_KEY / LLM_BASE_URL / LLM_MODEL
1. 环境变量(最高):LLM_API_KEY / LLM_BASE_URL / LLM_MODEL,
或 ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL / ANTHROPIC_MODEL
2. 配置文件(中间):web/backend/configs/llm.yaml
3. 内置默认值(最低)
...
...
@@ -26,29 +22,15 @@ import json
import
logging
import
os
import
re
from
dataclasses
import
dataclass
,
field
from
dataclasses
import
dataclass
from
pathlib
import
Path
from
typing
import
Any
,
Optional
logger
=
logging
.
getLogger
(
__name__
)
# ── Provider 预设 ────────────────────────────────────────
# 各 Provider 的默认 base_url 与模型,避免每次都填
PROVIDER_PRESETS
:
dict
[
str
,
dict
]
=
{
"minimax"
:
{
"base_url"
:
"https://api.minimaxi.com/anthropic"
,
"model"
:
"MiniMax-Text-01"
,
},
"anthropic"
:
{
"base_url"
:
""
,
# 不填,使用官方默认
"model"
:
"claude-sonnet-5"
,
},
"openai"
:
{
"base_url"
:
""
,
# 不填,使用官方默认
"model"
:
"gpt-4o"
,
},
}
DEFAULT_BASE_URL
=
"http://192.168.40.199:8080"
DEFAULT_MODEL
=
"glm-5.3-flash"
# ── 配置文件路径 ─────────────────────────────────────────
...
...
@@ -68,21 +50,20 @@ def default_config_path() -> Path:
# ── 配置 ────────────────────────────────────────────────
@
dataclass
class
LLMConfig
:
provider
:
str
=
"minimax"
# minimax / anthropic / openai
api_key
:
Optional
[
str
]
=
None
base_url
:
Optional
[
str
]
=
None
# 留空则按 provider 预设
model
:
Optional
[
str
]
=
None
# 留空则按 provider 预设
max_tokens
:
int
=
1024
base_url
:
Optional
[
str
]
=
None
model
:
Optional
[
str
]
=
None
max_tokens
:
int
=
2048
temperature
:
float
=
0.2
timeout
:
int
=
30
@
property
def
effective_model
(
self
)
->
str
:
return
self
.
model
or
PROVIDER_PRESETS
.
get
(
self
.
provider
,
{}).
get
(
"model"
,
"claude-sonnet-5"
)
return
self
.
model
or
DEFAULT_MODEL
@
property
def
effective_base_url
(
self
)
->
str
:
return
self
.
base_url
or
PROVIDER_PRESETS
.
get
(
self
.
provider
,
{}).
get
(
"base_url"
,
""
)
return
self
.
base_url
or
DEFAULT_BASE_URL
@
property
def
available
(
self
)
->
bool
:
...
...
@@ -93,7 +74,7 @@ class LLMConfig:
"""三段式加载:默认值 < 配置文件 < 环境变量(环境变量最高优先)
优先级说明:
- 文件配置:方便项目级持久化(推荐把
provider
/ model / base_url 写这里)
- 文件配置:方便项目级持久化(推荐把
api_key
/ model / base_url 写这里)
- 环境变量:方便临时切换 / 容器化部署 / CI
"""
path
=
config_path
or
default_config_path
()
...
...
@@ -106,25 +87,22 @@ class LLMConfig:
if
v
is
not
None
and
hasattr
(
cfg
,
k
):
setattr
(
cfg
,
k
,
v
)
# 2. 先把 provider 应用(再决定从哪个专用 env 拿 key)
env_provider
=
os
.
environ
.
get
(
"LLM_PROVIDER"
)
if
env_provider
:
cfg
.
provider
=
env_provider
# 3. 环境变量(最高)
# 2. 环境变量(最高)
env_overrides
:
dict
[
str
,
Any
]
=
{
"api_key"
:
os
.
environ
.
get
(
"LLM_API_KEY"
),
"base_url"
:
os
.
environ
.
get
(
"LLM_BASE_URL"
),
"model"
:
os
.
environ
.
get
(
"LLM_MODEL"
),
}
if
env_overrides
[
"api_key"
]
is
None
:
# 兼容各 provider 的专用 env var(以更新后的 provider 为准)
if
cfg
.
provider
==
"anthropic"
:
env_overrides
[
"api_key"
]
=
os
.
environ
.
get
(
"ANTHROPIC_API_KEY"
)
elif
cfg
.
provider
==
"openai"
:
env_overrides
[
"api_key"
]
=
os
.
environ
.
get
(
"OPENAI_API_KEY"
)
elif
cfg
.
provider
==
"minimax"
:
env_overrides
[
"api_key"
]
=
os
.
environ
.
get
(
"MINIMAX_API_KEY"
)
env_overrides
[
"api_key"
]
=
(
os
.
environ
.
get
(
"ANTHROPIC_API_KEY"
)
or
os
.
environ
.
get
(
"ANTHROPIC_AUTH_TOKEN"
)
)
if
env_overrides
[
"base_url"
]
is
None
:
env_overrides
[
"base_url"
]
=
os
.
environ
.
get
(
"ANTHROPIC_BASE_URL"
)
if
env_overrides
[
"model"
]
is
None
:
env_overrides
[
"model"
]
=
os
.
environ
.
get
(
"ANTHROPIC_MODEL"
)
env_max_tokens
=
os
.
environ
.
get
(
"LLM_MAX_TOKENS"
)
if
env_max_tokens
:
...
...
@@ -170,94 +148,88 @@ class LLMClient:
self
.
_provider
=
None
if
self
.
cfg
.
available
:
try
:
self
.
_provider
=
self
.
_init_
provider
()
self
.
_provider
=
self
.
_init_
client
()
logger
.
info
(
f"LLM 客户端就绪: provider=
{
self
.
cfg
.
provider
}
, "
f"model=
{
self
.
cfg
.
effective_model
}
, "
f"LLM 客户端就绪: model=
{
self
.
cfg
.
effective_model
}
, "
f"base_url=
{
self
.
cfg
.
effective_base_url
or
'默认'
}
"
)
except
Exception
as
e
:
logger
.
warning
(
f"LLM 初始化失败:
{
e
}
"
)
else
:
logger
.
info
(
f"LLM 未配置 API Key(provider=
{
self
.
cfg
.
provider
}
)
,"
f
"如需启用请在 web/backend/configs/llm.yaml 设置 api_key 或配置环境变量"
"LLM 未配置 API Key
,"
"如需启用请在 web/backend/configs/llm.yaml 设置 api_key 或配置环境变量"
)
def
_init_provider
(
self
):
"""根据 provider 创建对应 SDK 客户端"""
provider
=
self
.
cfg
.
provider
def
_init_client
(
self
):
"""创建 Anthropic 兼容协议客户端。"""
api_key
=
self
.
cfg
.
api_key
base_url
=
self
.
cfg
.
effective_base_url
or
None
if
provider
in
(
"anthropic"
,
"minimax"
):
# MiniMax 用 Anthropic SDK + 自定义 base_url
import
anthropic
kwargs
:
dict
[
str
,
Any
]
=
{
"api_key"
:
api_key
,
"timeout"
:
self
.
cfg
.
timeout
,
}
if
base_url
:
kwargs
[
"base_url"
]
=
base_url
return
anthropic
.
Anthropic
(
**
kwargs
)
elif
provider
==
"openai"
:
import
openai
kwargs
=
{
"api_key"
:
api_key
,
"timeout"
:
self
.
cfg
.
timeout
}
if
base_url
:
kwargs
[
"base_url"
]
=
base_url
return
openai
.
OpenAI
(
**
kwargs
)
raise
ValueError
(
f"不支持的 LLM provider:
{
provider
!
r
}
(目前支持 minimax / anthropic / openai)"
)
import
anthropic
kwargs
:
dict
[
str
,
Any
]
=
{
"api_key"
:
api_key
,
"timeout"
:
self
.
cfg
.
timeout
,
# 请求失败由业务接口直接反馈,避免 SDK 在后台重试导致页面长时间无响应。
"max_retries"
:
0
,
}
if
base_url
:
kwargs
[
"base_url"
]
=
base_url
return
anthropic
.
Anthropic
(
**
kwargs
)
@
property
def
available
(
self
)
->
bool
:
return
self
.
_provider
is
not
None
# ── 核心调用 ──
def
complete
(
self
,
prompt
:
str
,
system
:
str
=
""
,
json_mode
:
bool
=
False
)
->
str
:
def
complete
(
self
,
prompt
:
str
,
system
:
str
=
""
,
json_mode
:
bool
=
False
,
max_tokens
:
int
|
None
=
None
,
)
->
str
:
"""调用 LLM,返回纯文本。失败抛 LLMUnavailable(携带可读的错误信息)。"""
if
not
self
.
available
:
raise
LLMUnavailable
(
"LLM 客户端未配置 API Key"
)
try
:
if
self
.
cfg
.
provider
in
(
"anthropic"
,
"minimax"
):
kwargs
:
dict
[
str
,
Any
]
=
{
"model"
:
self
.
cfg
.
model
,
"max_tokens"
:
self
.
cfg
.
max_tokens
,
"messages"
:
[{
"role"
:
"user"
,
"content"
:
prompt
}],
}
# 当前 Anthropic SDK 1.x 的 Messages API 已不接收 temperature。
# MiniMax 走同一兼容接口,传入会在本地 SDK 参数校验阶段失败。
if
system
:
kwargs
[
"system"
]
=
system
msg
=
self
.
_provider
.
messages
.
create
(
**
kwargs
)
return
msg
.
content
[
0
].
text
# openai
kwargs
=
{
"model"
:
self
.
cfg
.
model
,
"max_tokens"
:
self
.
cfg
.
max_tokens
,
"temperature"
:
self
.
cfg
.
temperature
,
"messages"
:
[{
"role"
:
"system"
,
"content"
:
system
},
{
"role"
:
"user"
,
"content"
:
prompt
}],
base_kwargs
:
dict
[
str
,
Any
]
=
{
"model"
:
self
.
cfg
.
effective_model
,
"messages"
:
[{
"role"
:
"user"
,
"content"
:
prompt
}],
}
if
json_mode
:
kwargs
[
"response_format"
]
=
{
"type"
:
"json_object"
}
resp
=
self
.
_provider
.
chat
.
completions
.
create
(
**
kwargs
)
return
resp
.
choices
[
0
].
message
.
content
# 未支持的 provider——按理 _init_provider 已拦过,这里再防一次
raise
LLMUnavailable
(
f"complete() 未实现 provider=
{
self
.
cfg
.
provider
!
r
}
的调用分支"
# 当前 Anthropic SDK 1.x 的 Messages API 已不接收 temperature。
if
system
:
base_kwargs
[
"system"
]
=
system
# 部分兼容服务会先输出 thinking 块。第一次额度不够时,继续请求一次,
# 让模型有足够预算输出最终文本,而不是把 thinking 当作最终结果返回。
initial_budget
=
max_tokens
or
self
.
cfg
.
max_tokens
retry_budget
=
min
(
initial_budget
*
2
,
self
.
cfg
.
max_tokens
)
token_budgets
=
(
(
initial_budget
,
retry_budget
)
if
retry_budget
>
initial_budget
else
(
initial_budget
,)
)
for
attempt
,
max_tokens
in
enumerate
(
token_budgets
,
start
=
1
):
kwargs
=
{
**
base_kwargs
,
"max_tokens"
:
max_tokens
}
msg
=
self
.
_provider
.
messages
.
create
(
**
kwargs
)
for
block
in
msg
.
content
:
text
=
getattr
(
block
,
"text"
,
None
)
if
isinstance
(
text
,
str
):
return
text
if
attempt
==
1
and
len
(
token_budgets
)
>
1
:
logger
.
info
(
"LLM 仅返回思考块,使用更高输出额度重试 "
f"(max_tokens=
{
token_budgets
[
1
]
}
)"
)
raise
LLMUnavailable
(
"LLM 响应中没有可用的文本内容"
)
except
LLMUnavailable
:
raise
except
Exception
as
e
:
detail
=
_format_api_error
(
e
,
self
.
cfg
.
provider
)
detail
=
_format_api_error
(
e
)
logger
.
warning
(
f"LLM 调用失败 (provider=
{
self
.
cfg
.
provider
}
, "
f"model=
{
self
.
cfg
.
effective_model
}
):
{
detail
}
"
f"LLM 调用失败 (model=
{
self
.
cfg
.
effective_model
}
):
{
detail
}
"
)
raise
LLMUnavailable
(
detail
)
...
...
@@ -272,16 +244,16 @@ class LLMClient:
# ── API 错误信息提取 ──────────────────────────────────────
def
_format_api_error
(
exc
:
Exception
,
provider
:
str
)
->
str
:
def
_format_api_error
(
exc
:
Exception
)
->
str
:
"""从 SDK 异常中提取可读的错误信息,供前端实时日志展示。
优先处理 Anthropic
/OpenAI
SDK 的结构化异常,
优先处理 Anthropic SDK 的结构化异常,
兜底处理网络/超时等通用异常。
"""
exc_type
=
type
(
exc
).
__name__
exc_msg
=
str
(
exc
)
# ── Anthropic SDK 异常
(MiniMax 兼容接口也走这里)
──
# ── Anthropic SDK 异常
───────────────────────────────
──
try
:
from
anthropic
import
(
APIStatusError
,
...
...
@@ -329,33 +301,6 @@ def _format_api_error(exc: Exception, provider: str) -> str:
except
ImportError
:
pass
# ── OpenAI SDK 异常 ──
try
:
from
openai
import
(
APIStatusError
as
OAIStatusError
,
RateLimitError
as
OAIRateLimitError
,
AuthenticationError
as
OAIAuthError
,
APIConnectionError
as
OAIConnectionError
,
APITimeoutError
as
OAITimeoutError
,
)
if
isinstance
(
exc
,
OAIRateLimitError
):
return
(
f"API Error: 请求被限流 (429) ·
{
exc_msg
}
"
)
if
isinstance
(
exc
,
OAIAuthError
):
return
(
f"API Error: 认证失败 (401) · 请检查 API Key 是否正确或已过期"
)
if
isinstance
(
exc
,
OAIStatusError
):
status
=
getattr
(
exc
,
"status_code"
,
"?"
)
return
f"API Error: 请求失败 (
{
status
}
) ·
{
exc_msg
}
"
if
isinstance
(
exc
,
OAIConnectionError
):
return
f"API Error: 网络连接失败 ·
{
exc_msg
}
"
if
isinstance
(
exc
,
OAITimeoutError
):
return
f"API Error: 请求超时 ·
{
exc_msg
}
"
except
ImportError
:
pass
# ── 通用网络/超时异常 ──
import
builtins
if
isinstance
(
exc
,
builtins
.
ConnectionError
):
...
...
web/backend/requirements.txt
View file @
bd79c9e5
...
...
@@ -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
web/backend/routers/ai.py
View file @
bd79c9e5
...
...
@@ -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
:.
0
f
}
ms)"
)
...
...
web/src/views/RuleLibraryView.vue
View file @
bd79c9e5
...
...
@@ -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
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment