神龙十问 LKeAP JSON 生成质量调研
调研日期:2026-04-11 调研背景:神龙十问切到 Tencent LKeAP (
/plan/v3/chat/completions) 后报Upstream error 400涉及代码:service/ai_chat_service.py:analyze_vuln_questions()(line 462+)、configs/settings.py:117-133状态:调研完成 + 修复已应用
TL;DR
| 维度 | 结论 |
|---|---|
| 400 根因 | 默认模型 deepseek-v3 不在 /plan/v3 endpoint 的可用模型列表里,每次调用都是 invalid model (code 20033) |
| 错误日志 bug | service/ai_chat_service.py:630 只 yield 了 status code,没透出 response body,导致前端只能看到孤零零的 Upstream error 400 |
| 选定模型 | hunyuan-2.0-instruct —— 速度快(~12s vs turbos 的 ~30s)、内容质量到位、失败模式良性 |
| 真实数据通过率 | 40 次跨 20 个 CVE 的生产数据测试:strict 90% / lenient 92.5%(lenient 即代码加 strict=False 后的实际通过率) |
| 不要做的事 | 不要换 prompt 去"强化 JSON 规则"——实测对多数模型反而是负面优化 |
| 真正的修复 | 代码侧用 json.loads(s, strict=False) 容错 + 解析后规范化再入库;模型/prompt 调到合理值即可 |
1. 故障定位
1.1 用户报告
神龙十问也不行:{"error": "Upstream error 400"}
1.2 第一个发现:错误信息丢失
service/ai_chat_service.py:628-631:
with requests.post(lkeap_url, headers=headers, json=payload, stream=True, timeout=120) as response:
if response.status_code != 200:
yield f"data: {json.dumps({'error': f'Upstream error {response.status_code}'})}
"
return
错误处理只读了 status_code,完全没读 response.text 或 response.content。导致前端永远看不到上游的具体错误。这是后续所有定位困难的元凶。
1.3 直接 curl 上游
curl -sS -X POST 'https://api.lkeap.cloud.tencent.com/plan/v3/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-tp-...' \
-d '{"model":"deepseek-v3","messages":[{"role":"user","content":"hi"}],"stream":false,"max_tokens":50}'
返回:
{"id":"0ba4c8b63fa1c12c87ea3ebcf8aea379",
"error":{"message":"invalid model","type":"runtime_error","param":null,"code":"20033"}}
HTTP 400。模型名错了。
1.4 发现可用模型列表
curl -sS 'https://api.lkeap.cloud.tencent.com/plan/v3/models' \
-H 'Authorization: Bearer sk-tp-...'
返回的可用模型:
minimax-m-2-5 / minimax-m2.5
glm-5-0 / glm-5
kimi-k-2-5 / kimi-k2.5
tc-code-latest
hunyuan-turbos
hunyuan-t1
hunyuan-2.0-instruct
hunyuan-2.0-thinking
注意:列表里完全没有 deepseek-* 系列。/plan/v3 是 LKeAP 的"plan"版本(相对于标准 /v1),托管的是另一组模型——主要是腾讯混元 + 三方品牌(minimax / kimi / glm)。
同步实验:
https://api.lkeap.cloud.tencent.com/v1/chat/completions用同一个 key 返回401 not_authorized,说明这个 key 是 plan 套餐专用 key,不能用标准 endpoint。https://api.lkeap.cloud.tencent.com/v1/models同样 401。
也就是说环境完全锁死在 /plan/v3 + 上述 8 个模型,不能跨 endpoint 切回 deepseek。
2. 模型质量初测
2.1 测试方法
完全复刻生产 prompt(service/ai_chat_service.py:567-594 中文分支),输入是真实结构的 CVE JSON(query_cve_details 输出形态),要求输出 Q1-Q10 的 JSON 对象。
判定标准:
strict:json.loads(content)直接通过lenient:json.loads(content, strict=False)通过(允许字符串字面里出现 LF/CR/Tab 等控制字符)keys:是否齐全 Q1-Q10 共 10 个键strings:所有 value 是否都是 string
2.2 hunyuan-turbos 首测
第一次测 hunyuan-turbos 在 Log4j 上,HTTP 200 / 30 秒,但解析失败:
JSON parse FAILED: Invalid control character at: line 3 column 35 (char 63)
更糟的是结构本身就崩了:
{
"Q1": "这个漏洞是什么?(本质+后果)",
"**Apache Log4j2 远程代码执行漏洞!** 💥
**本质**:JNDI 功能未过滤攻击者可控输入。
**后果**:攻击者可远程执行任意代码!🚨",
"Q2": "根本原因?(CWE/缺陷点)",
"**JNDI 注入漏洞!** ⚠️
..."
}
模型把"问题文本"当成 value,把答案塞成无 key 的悬空字符串——双层错误。结合 30s 的高延迟,turbos 直接淘汰。
2.3 hunyuan-2.0-instruct 首测
| CVE | 时间 | 结果 |
|---|---|---|
| CVE-2021-44228 | 14s | ✅ strict OK |
| CVE-2023-23397 | 10s | ✅ strict OK |
| CVE-2024-3094 | 11s | ❌ JSON FAIL Invalid control character at: line 2 column 44 |
XZ 这个 case 内容质量很好,但每个 string value 都用了字面 LF 换行:
"Q1": "- **本质**:恶意代码藏进 `xz` 源码(5.6.0起)📦
- 构建时提取伪装的测试文件 → 生成对象文件 🧩
- **后果**:篡改 `liblzma` → 所有链接它的软件被劫持 🚨",
JSON 标准里 string 内不允许字面 LF,必须 。此时 json.loads(s) 抛 Invalid control character。
关键观察:内容是对的、键是对的、结构是对的——只是 LF 没转义。
3. 6 模型 × 2 prompt 横评
3.1 测试矩阵
模型:minimax-m2.5、kimi-k2.5、glm-5、hunyuan-2.0-instruct、hunyuan-2.0-thinking、hunyuan-t1
Prompt 变体:
- Baseline:当前生产 prompt(
analyze_vuln_questionsline 567-594 原样) - Strict:显式给 schema、显式禁止字面换行、显式给 JSON 规则编号
参数:temperature=0.3、max_tokens=2500、stream=False、response_format={"type":"json_object"}
测试 CVE:CVE-2024-3094(前一轮 instruct 唯一翻车的那个)
3.2 Pass 1: Baseline prompt
| 模型 | 时间 | tokens_out | strict | lenient | 备注 |
|---|---|---|---|---|---|
| minimax-m2.5 | 26.2s | 1214 | ✅ | ✅ | avg 110 字符,内容最长 |
| kimi-k2.5 | 90.8s | 2500 | ❌ | ❌ | 撞 max_tokens,输出不是 JSON |
| glm-5 | 48.3s | 2500 | ❌ | ❌ | 撞 max_tokens,输出截断 |
| hunyuan-2.0-instruct | 13.6s | 748 | ✅ | ✅ | avg 91 字符 |
| hunyuan-2.0-thinking | 22.9s | 1945 | ✅ | ✅ | avg 92 字符 |
| hunyuan-t1 | 33.1s | 2091 | ✅ | ✅ | avg 53 字符(偏短) |
要点:
- kimi-k2.5 / glm-5 不遵守
response_format=json_object——它们继续输出富文本(带推理过程或自由发挥),直接撞 max_tokens 上限。这两个对当前用法不可用。 - 其余 4 个 baseline 都过了。
3.3 Pass 2: Strict prompt
Strict prompt 增加的约束:
# JSON RULES — VIOLATIONS WILL BREAK THE PARSER
1. Output ONLY the JSON object. No prose before/after. No ```json fence.
2. Each value is a SINGLE-LINE JSON string. Multi-line content MUST use the
literal two-character sequence backslash + n (
) for line breaks.
3. NEVER place a raw newline (LF / CR) inside a string value.
4. Escape any double-quote inside a value as \".
5. Keys are exactly Q1, Q2, ..., Q10.
6. All 10 keys are required.
| 模型 | 时间 | strict | lenient | 备注 |
|---|---|---|---|---|
| minimax-m2.5 | 20.2s | ❌ | ❌ | 被搞崩了,Expecting ',' delimiter |
| kimi-k2.5 | 91.0s | ❌ | ❌ | 仍撞 max_tokens |
| glm-5 | 46.6s | ❌ | ❌ | 仍撞 max_tokens |
| hunyuan-2.0-instruct | 7.5s | ✅ | ✅ | avg 跌到 42 字符——内容被压缩太短 |
| hunyuan-2.0-thinking | 18.8s | ✅ | ✅ | avg 68(也变短) |
| hunyuan-t1 | 34.1s | ❌ | ❌ | 被搞崩了 |
3.4 横评结论
严化 prompt 是负面优化。
- 让原本能跑通的 minimax 和 t1 直接 break
- 让 instruct 变得过度简洁(42 字符 avg),失去神龙十问该有的内容密度
- 对真正问题(字面换行符)零修复——instruct 之所以"看起来"通过了 strict prompt 测试,实际是因为内容被压短到根本没换行
模型选型基本锁定在 hunyuan-2.0-instruct、hunyuan-2.0-thinking,prompt 维持现状。
4. 合成数据稳定性测试
4.1 设计
模型 × 温度组合:
hunyuan-2.0-instruct @ T=0.3hunyuan-2.0-instruct @ T=0.5(生产当前值)hunyuan-2.0-thinking @ T=0.3
每个组合 × 3 个 CVE × 3 次 run = 27 次调用。
4.2 汇总
| 配置 | strict 通过 | 平均延迟 | 平均字符 | 失败模式 |
|---|---|---|---|---|
| instruct @ T=0.3 | 7/9 (78%) | 11.8s | 94 | 全是字面换行(lenient 100% 通过) |
| instruct @ T=0.5 | 6/9 (67%) | 10.6s | 90 | 全是字面换行(lenient 100% 通过) |
| thinking @ T=0.3 | 7/9 (78%) | 20.4s | 77 | 2 次结构性硬崩(lenient 也过不了) |
4.3 决定性观察
- 没有任何模型能 100% 严格 JSON。即使 instruct + T=0.3,9 次里仍有 2 次字面换行。这是 LLM 的本质特性,不是 prompt 工程能根治的。
- instruct 的失败 100% 是良性的——所有 strict 失败的 case 全部能 lenient parse。
json.loads(s, strict=False)一行代码全部救回。 - thinking 的失败是恶性的——会出结构损坏(kv 错位、引号丢失等),lenient 都救不回。同样 78% 通过率,但失败是真失败。
- 温度 0.3 vs 0.5:通过率 78% vs 67%,11 个百分点的差距,足够支撑降温度。
- 延迟:instruct 11.8s ≈ thinking 20.4s 的 60%。
5. 真实数据大盘测试(决定性)
合成 CVE 描述毕竟简化,需要用 query_cve_details() 拉真实库数据(包括 POC + references + 完整 CVSS vector)做大盘测试。
5.1 测试集
20 个高影响力 CVE,覆盖 12 种不同漏洞类型:
| # | CVE | 通用名 | 类型 | 真实数据长度 |
|---|---|---|---|---|
| 1 | CVE-2021-44228 | Log4Shell | RCE/JNDI | 9872 chars |
| 2 | CVE-2014-0160 | Heartbleed | memory disclosure | 10365 chars |
| 3 | CVE-2024-3094 | XZ backdoor | supply chain | 10117 chars |
| 4 | CVE-2017-5638 | Struts2 OGNL | RCE | 7974 chars |
| 5 | CVE-2019-0708 | BlueKeep RDP | RCE wormable | 3760 chars |
| 6 | CVE-2021-26855 | ProxyLogon | SSRF/auth bypass | 8253 chars |
| 7 | CVE-2022-22965 | Spring4Shell | RCE | 10919 chars |
| 8 | CVE-2018-7600 | Drupalgeddon2 | RCE | 9544 chars |
| 9 | CVE-2020-1472 | Zerologon | crypto/privesc | 8853 chars |
| 10 | CVE-2023-44487 | HTTP/2 Rapid Reset | DoS | 9486 chars |
| 11 | CVE-2022-30190 | Follina MSDT | RCE | 6274 chars |
| 12 | CVE-2014-6271 | Shellshock | env injection | 7327 chars |
| 13 | CVE-2017-0144 | EternalBlue | SMB RCE | 9656 chars |
| 14 | CVE-2023-22515 | Confluence | auth bypass | 9487 chars |
| 15 | CVE-2018-1000861 | Jenkins Stapler | auth bypass | 3294 chars |
| 16 | CVE-2023-46805 | Ivanti Connect | auth bypass | 10184 chars |
| 17 | CVE-2024-21887 | Ivanti Connect | cmd injection | 5795 chars |
| 18 | CVE-2024-21413 | Outlook Moniker | NTLM leak | 11864 chars |
| 19 | CVE-2023-34362 | MOVEit Transfer | SQL injection | 7765 chars |
| 20 | CVE-2022-26134 | Confluence OGNL | RCE | 9703 chars |
每个 CVE 跑 2 次 = 40 次调用(hunyuan-2.0-instruct + T=0.3 + json_object)。
5.2 总指标
Total calls: 40, errors: 0, valid: 40
JSON quality:
strict pass: 36/40 (90.0%)
lenient pass: 37/40 (92.5%) ← 即代码加 strict=False 后的实际通过率
all Q1-Q10: 37/40 (92.5%)
all strings: 37/40 (92.5%)
Content metrics (37 well-formed responses):
avg_chars: 104 [min run 73, max run 140]
shortest answer field across all runs: 45 chars
longest answer field across all runs: 332 chars
empty/near-empty answers (<10 chars): 0 / 370 fields
latency: 12.0s [min 9.3, max 15.9]
tokens_out: 699 avg
Style compliance:
mentions CVE ID: 22/37 (59%)
has emojis: 37/37 (100%)
has bold markdown: 37/37 (100%)
has bullet points: 37/37 (100%)
5.3 失败分布
40 次结果分类:
- 36 次 strict 通过 —— 直接合法 JSON
- 1 次 strict 失败但 lenient 通过 —— 仅字面换行符问题,
strict=False救回 - 3 次 hard fail —— 结构性损坏,lenient 也救不回
3 次 hard fail 涉及的 CVE:
- CVE-2014-6271 Shellshock(run 2)
- CVE-2018-1000861 Jenkins Stapler(run 1)
- CVE-2024-21413 Outlook Moniker(run 1)
每个 CVE 第二次 run 都正常通过——说明 hard fail 是概率性偶发,不是 CVE 数据本身的问题。简单的"失败重试一次"策略可以把通过率推到接近 100%(当前未实现,留给后续优化)。
5.4 内容质量
风格合规率 100%:37 个有效响应全部满足风格指南——
- 100% 含 emoji
- 100% 用
**bold**markdown - 100% 用
-bullet points - 0 个空/近空答案(370 个字段全部有实质内容)
字符密度健康:
- 平均每答 104 字符
- 单答最短 45 字符(仍有完整意思),最长 332 字符(仍受 max_tokens 控制)
- 单次 run 平均 73-140 字符——稳定区间
CVE ID 显式提及率 59%:22/37 次响应在答案文本里提到了 CVE ID。剩余 41% 不提及不算问题——前端展示已经有大标题挂着 CVE ID,重复提没必要。
延迟均匀:9.3s - 15.9s,p50 ~12s。比之前用 deepseek 也没变慢。
5.5 抽样输出(首 3 个 CVE)
CVE-2021-44228 Log4Shell
Q1: **漏洞本质**:Apache Log4j 日志工具处理数据时触发📝。
- **后果**:攻击者可构造恶意请求 → 被记录 → 触发**远程代码执行(RCE)** 🚨。
- 服务器可被完全控制!
Q5: **利用门槛**:
- **极低** 🟢。
- 无需认证🔑。
- 只需让恶意字符串被日志记录📝。
- 常见入口:HTTP头、参数、聊天消息等。
Q6: **已有Exp**:
- ✅ 多个 PoC 公开 🧪。
- GitHub 示例:
- `tangxiaofeng7/CVE-2021-44228-Apache-Log4j-Rce` 💻。
- `jas502n/Log4j2-CVE-2021-44228` 💻。
- 已在野利用🚨。
Q9: **无补丁临时方案**:
- 设置系统属性:`log4j2.formatMsgNoLookups=True` 🛡️。
- 移除 `JndiLookup` 类 ❌。
- 用 RASP 工具拦截(如 `CloudArmor`)🛡️。
- 网络层阻断 LDAP/RMI 出站 🚧。
CVE-2014-0160 Heartbleed
Q1: 🚨 **漏洞本质**:OpenSSL处理TLS/DTLS心跳包时,**缺少边界检查**。
💥 **后果**:远程攻击者读取服务器内存敏感数据。
📂 可泄露:用户名、密码、Cookie、**私钥**等。
Q2: 🔍 **根本原因**:
- 位于`d1_both.c`和`t1_lib.c`。
- **未校验心跳包长度字段** ➜ 内存越界读取。
- 类似**CWE-125**(越界读取)。
Q6: 🧪 **现成PoC**:
- 🔗 [FiloSottile/Heartbleed](https://github.com/FiloSottile/Heartbleed) 🛠️检测工具。
- 🔗 [titanous/heartbleeder](https://github.com/titanous/heartbleeder) 等。
- 多PoC可用 ➜ **野外利用已出现**。
CVE-2024-3094 XZ Backdoor
Q1: **漏洞本质**:xz 5.6.0/5.6.1 被植入恶意代码 🚨
**后果**:攻击者可远程未授权访问系统 💻⚠️
Q2: **根本原因**:构建流程被篡改 🧩
- 恶意对象文件伪装成测试文件混入源码 📦
- 编译时修改 liblzma 函数逻辑 🛠️
- 类似 **供应链投毒**(CWE-506 内嵌后门)
Q6: **已有 PoC**:是 🔍
- GitHub 多个检测/利用脚本 🧪
- 示例:
- [CVE-2024-3094-checker](https://github.com/FabioBaroni/CVE-2024-3094-checker) 🔧
- [xzbot](https://github.com/amlweems/xzbot) 🤖
- 存在**在野利用迹象** 🚨
Q10: **优先级**:🔥极高!
- CVSS 3.1:**9.8** 🚨
- 影响广+易利用+可远程控制 🌐
- **立刻排查&降级** 🏃💨
结论:技术准确性、风格、可读性均达到生产可用标准。
5.6 是否够用?
够用。
- 92.5% 的实际通过率(lenient 模式下)
- 失败完全偶发、可通过重试覆盖
- 内容质量稳定,没有"通过但内容差"的情况
- 失败案例的下一次 run 都恢复正常
如果未来想推到 99%+,加 1 次失败重试即可——属于增量优化。
6. 最终方案 / 已应用的修改
6.1 改了 2 个文件 / 6 处
1. configs/settings.py:128
# Before
SHENLONG_ANALYSIS_MODEL = os.environ.get('SHENLONG_ANALYSIS_MODEL', 'deepseek-v3')
# After
SHENLONG_ANALYSIS_MODEL = os.environ.get('SHENLONG_ANALYSIS_MODEL', 'hunyuan-2.0-instruct')
2. service/ai_chat_service.py payload (~line 616)
payload = {
"model": lkeap_model,
"messages": messages,
"max_tokens": 2000,
"temperature": 0.3, # 0.5 → 0.3
"stream": True,
"response_format": {"type": "json_object"}, # 新增
}
3. service/ai_chat_service.py 上游错误处理 (~line 629)
# Before
if response.status_code != 200:
yield f"data: {json.dumps({'error': f'Upstream error {response.status_code}'})}
"
return
# After
if response.status_code != 200:
body = response.text[:500] if response.text else ''
error_msg = f"Upstream {response.status_code}: {body}"
print(f"[Shenlong] {error_msg}")
yield f"data: {json.dumps({'error': error_msg})}
"
return
4. service/ai_chat_service.py JSON 解析 + 入库前规范化 (~line 660)
# Before
try:
json.loads(clean_json) # validate only
if language == 'en':
analysis.questions_en = clean_json
else:
analysis.questions = clean_json
# After
try:
parsed = json.loads(clean_json, strict=False) # 容错
canonical_json = json.dumps(parsed, ensure_ascii=False) # 规范化
if language == 'en':
analysis.questions_en = canonical_json
else:
analysis.questions = canonical_json
同时把 yield 给前端的 complete 信号也改用 canonical_json,确保 frontend 最终态 = DB 持久态。
5. analyze_vuln_questions:482-489 缓存读取路径同步加 strict=False:
json.loads(analysis.questions_en, strict=False)
json.loads(analysis.questions, strict=False)
兜底防御——历史 record 可能也存了带字面换行的 raw 文本。
6.2 不做的事
| 不做 | 原因 |
|---|---|
| ❌ 强化 prompt 加 JSON 规则 | 横评里实测让 minimax / t1 直接崩、让 instruct 输出过短 |
| ❌ 切到 thinking | 同样 78% 通过率,但失败是结构性硬崩,lenient 救不回;延迟还多一倍 |
| ❌ 切到 minimax-m2.5 | baseline 能过但 26s 太慢,是 instruct 的 2 倍 |
| ❌ 用 kimi-k2.5 / glm-5 | 不遵守 response_format=json_object,撞 max_tokens |
| ❌ 调回 deepseek | API key 锁死在 /plan/v3 plan 套餐,没有 deepseek |
| ❌ 自己写正则去 escape 换行 | Python 内置 strict=False 直接覆盖此场景,不要造轮子 |
| ❌ 加失败重试逻辑 | 增量优化,本次范围之外;92.5% 已经够用 |
7. 经验沉淀
7.1 关于 LLM JSON 输出
- **永远不要假设 LLM 输出 100% 严格 JSON。**生产侧必须有容错路径。
json.loads(s, strict=False)是 Python 的免费午餐——专门覆盖"字符串里有字面控制字符"这一类最常见的 LLM 失败模式。- 解析后规范化再持久化(
json.loads → json.dumps),不要把 raw 文本写库,否则缓存读取路径会重复踩同一个坑。 - 温度有用但有限:T=0.3 比 T=0.5 通过率高 ~10pp,但不是决定性的。容错代码才是决定性的。
- 大盘测试不能省:合成数据的 27 次测试给出 78% 通过率;真实数据 40 次给出 92.5%。差距来自 prompt 里 cve_data 的真实长度和复杂度——合成数据偏简化,模型反而更容易在简单文本上偏离格式。
7.2 关于错误日志
status_code不是错误信息。任何 HTTP 错误处理路径都必须 capture body(至少前 500 字节)。这次问题如果第一时间能看到invalid model code 20033,定位时间能从一小时压缩到一分钟。
7.3 关于 prompt 工程
- 加规则不一定提升输出质量——有时候反而是负面优化。本次 strict prompt 在 6 个模型上:1 个变好(其实只是变短)、3 个崩、2 个变短。
- prompt 能改善的是"模型理解任务",治不了"模型偏离格式"。后者属于解码层问题,要么靠 grammar-constrained decoding(LKeAP 不支持),要么靠
response_format=json_object(部分模型也不遵守),要么靠代码侧容错。
7.4 关于 /plan/v3 这个 endpoint
- 它是 LKeAP 的 plan 套餐 endpoint,对应一组国内合规模型:腾讯混元 + minimax + kimi + glm + tc-code。
- 不包含 DeepSeek。如果业务方说"我要 deepseek-v3",要么换 endpoint(标准
/v1,需要不同的 key),要么换模型。 - API key 前缀
sk-tp-是 plan 套餐专用 key,不能跨 endpoint 使用。
8. 附:测试脚本
调研期间的脚本(都在 /tmp/,下次用前最好复制到项目里 archive):
| 文件 | 用途 |
|---|---|
/tmp/test_hunyuan.py | hunyuan-turbos 单点测试(首次发现问题) |
/tmp/test_models.py | hunyuan-2.0-instruct 单模型测试 |
/tmp/test_compare.py | 6 模型 × 2 prompt 横评 |
/tmp/test_stability.py | 27 次合成数据稳定性测试 |
/tmp/test_realdata.py | 40 次真实库数据大盘测试(决定性数据) |
/tmp/shenlong_samples.json | 18 个 CVE 的完整样本输出 |