chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交 (5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。 一、docs 结构整改(整改 #14) 根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入, 随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录 树并存于 docs/,共 791 文件、双分类体系冲突。 修复动作: - b2 同名异主题文件改名迁移保全 9 个 - C 类 39 个孤立文件按主题正确归类 - A/B1 类 222 个重复文件删除(新结构已有内容副本) - 9 个旧独有空目录删除 - 270 处内部引用按 verified 映射改写 - 整改记录 #14 登记于 04-运维文档/部署运维 结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。 残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。 二、compose 双目录对齐(消除踩坑 A) - docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist 改为 src/frontend-*/dist(h5 / agent / admin / terminal) - docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/ - 效果:本地 docker compose up 不再把根目录 stale dist 挂回, 与线上一致,分叉隐患消除(已 docker compose config 校验通过) 防复发铁律: - 重构须提交;仓库修复须 git stash -u 或先 commit - 新结构须 git add 并提交,避免再次 untracked 复活 - H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Build itdesk_main_v3_feedback-vars_DRAFT.yml from itdesk_main_v2_result.yml
|
||||
Preserves outer JSON wrapper + full inner YAML (app, kind, version, dependencies, workflow, etc.)
|
||||
"""
|
||||
import json, yaml
|
||||
|
||||
SRC = r'd:\资料\03-项目开发\wecom_it_smart_desk\docs\02-技术文档\实现配置\dify_dsl\itdesk_main_v2_result.yml'
|
||||
DST = r'd:\资料\03-项目开发\wecom_it_smart_desk\docs\02-技术文档\实现配置\dify_dsl\itdesk_main_v3_feedback-vars_DRAFT.yml'
|
||||
|
||||
# ---------- Load ----------
|
||||
raw = open(SRC, encoding='utf-8').read()
|
||||
top = json.loads(raw)
|
||||
inner = yaml.safe_load(top['data']) # full inner YAML dict (has app, kind, version, dependencies, workflow, ...)
|
||||
assert 'workflow' in inner, 'inner YAML missing workflow key'
|
||||
|
||||
workflow = inner['workflow']
|
||||
graph = workflow['graph']
|
||||
|
||||
# ---------- 1. start node: add 5 variables ----------
|
||||
start = next(n for n in graph['nodes'] if n.get('data', {}).get('type') == 'start')
|
||||
start_vars = [
|
||||
{"label": "反馈类型", "max_length": 32, "options": [], "required": False, "type": "text-input", "variable": "feedback_type"},
|
||||
{"label": "题目稳定标识", "max_length": 64, "options": [], "required": False, "type": "text-input", "variable": "question_id"},
|
||||
{"label": "选项稳定标识", "max_length": 64, "options": [], "required": False, "type": "text-input", "variable": "option_id"},
|
||||
{"label": "选项 value", "max_length": 64, "options": [], "required": False, "type": "text-input", "variable": "option_value"},
|
||||
{"label": "选项 label(已 mask)", "max_length": 200, "options": [], "required": False, "type": "text-input", "variable": "option_label"},
|
||||
]
|
||||
start['data']['variables'] = start_vars
|
||||
print(f'[OK] start.variables patched: {len(start_vars)} variables added')
|
||||
|
||||
# ---------- 2. system prompt: append options spec to all LLM nodes ----------
|
||||
OPTIONS_SPEC = """
|
||||
|
||||
### REQ-通用-005 v2.1 选项输出规范
|
||||
|
||||
当输出结构化推荐选项时(reply_type=question_card 或带 options[] 的场景),每条 option 必须同时输出:
|
||||
- `label`:选项显示文本
|
||||
- `value`:选项 value(程序内引用)
|
||||
- `question_id`:题目稳定标识(同一题所有选项共享一个 question_id;不同时期待二次追问的选项 question_id 应当不同)
|
||||
- `option_id`:当前选项稳定标识
|
||||
|
||||
反序列化示例:
|
||||
{
|
||||
"reply_type": "question_card",
|
||||
"options": [
|
||||
{"label": "VPN 连不上", "value": "vpn_failed", "question_id": "network_diagnosis", "option_id": "vpn_failed"}
|
||||
]
|
||||
}
|
||||
|
||||
question_id/option_id 必须稳定且唯一,用于前端选项历史匹配与坐席可见性追溯。不允许只输出 label。"""
|
||||
|
||||
llm_nodes = [n for n in graph['nodes'] if n.get('data', {}).get('type') == 'llm']
|
||||
patched_sys = 0
|
||||
for n in llm_nodes:
|
||||
sp = n['data'].get('prompt_template', [])
|
||||
for block in sp:
|
||||
if isinstance(block, dict) and block.get('role') == 'system':
|
||||
t = block.get('text', '')
|
||||
if 'REQ-通用-005 v2.1' not in t: # idempotency guard
|
||||
block['text'] = t + OPTIONS_SPEC
|
||||
patched_sys += 1
|
||||
break
|
||||
print(f'[OK] system prompt patched on {patched_sys}/9 LLM nodes (options spec)')
|
||||
|
||||
# ---------- 3. user prompt: append feedback injection to the 2 main LLM nodes ----------
|
||||
FEEDBACK_BLOCK = """
|
||||
|
||||
【用户上一轮反馈(来自开始节点)】
|
||||
- feedback_type: {{#sys.feedback_type#}}
|
||||
- question_id: {{#sys.question_id#}}
|
||||
- option_id: {{#sys.option_id#}}
|
||||
- option_value: {{#sys.option_value#}}
|
||||
- option_label: {{#sys.option_label#}}
|
||||
|
||||
如果 feedback_type == "option_select",说明用户上一轮点击了 question_id={{#sys.question_id#}} 的 option_id={{#sys.option_id#}}({{#sys.option_label#}})。请据此继续追问、分诊或给出针对性建议,不要重新走首次问候流程。
|
||||
如果 feedback_type 为空,按首次提问正常处理。
|
||||
|
||||
注意:option_label 来自前端 H5 模板,已经过敏感词 mask(如手机号中间四位变成 ****),请勿尝试"还原"原值,以业务常识继续。"""
|
||||
|
||||
# Two main LLM node ids (producers feeding the 整合回复 answer nodes)
|
||||
MAIN_LLM_IDS = {'1764905844059', '17657866286790'}
|
||||
|
||||
patched_usr = 0
|
||||
for n in llm_nodes:
|
||||
if n['id'] not in MAIN_LLM_IDS:
|
||||
continue
|
||||
sp = n['data'].get('prompt_template', [])
|
||||
for block in sp:
|
||||
if isinstance(block, dict) and block.get('role') == 'user':
|
||||
t = block.get('text', '')
|
||||
if 'REQ-通用-005 反馈注入' not in t: # idempotency guard
|
||||
block['text'] = t + FEEDBACK_BLOCK
|
||||
patched_usr += 1
|
||||
break
|
||||
print(f'[OK] user prompt patched on {patched_usr}/2 main LLM nodes (feedback injection)')
|
||||
|
||||
# ---------- dump back (preserves full inner YAML) ----------
|
||||
inner_yaml = yaml.safe_dump(inner, allow_unicode=True, sort_keys=False, width=10000)
|
||||
top['data'] = inner_yaml.rstrip('\n') + '\n' # keep parity with v2 file ending (single trailing newline)
|
||||
out = json.dumps(top, ensure_ascii=False, separators=(',', ':'))
|
||||
if not out.endswith('\n'):
|
||||
out += '\n'
|
||||
open(DST, 'w', encoding='utf-8').write(out)
|
||||
print(f'\n[OK] Wrote {DST} ({len(out)} bytes)')
|
||||
|
||||
# ---------- Verify ----------
|
||||
verify_top = json.loads(open(DST, encoding='utf-8').read())
|
||||
verify_inner = yaml.safe_load(verify_top['data'])
|
||||
print(f"[VERIFY] inner top keys: {list(verify_inner.keys())}")
|
||||
verify_wf = verify_inner['workflow']
|
||||
verify_start = next(n for n in verify_wf['graph']['nodes'] if n['data']['type'] == 'start')
|
||||
print(f"[VERIFY] start.variables count: {len(verify_start['data']['variables'])}")
|
||||
optcount = fbcount = 0
|
||||
for n in verify_wf['graph']['nodes']:
|
||||
if n['data'].get('type') != 'llm':
|
||||
continue
|
||||
for b in n['data'].get('prompt_template', []):
|
||||
if isinstance(b, dict):
|
||||
if 'REQ-通用-005 v2.1' in b.get('text', ''):
|
||||
optcount += 1
|
||||
if 'REQ-通用-005 反馈注入' in b.get('text', ''):
|
||||
fbcount += 1
|
||||
print(f"[VERIFY] LLM nodes with options spec: {optcount}")
|
||||
print(f"[VERIFY] LLM nodes with feedback injection: {fbcount}")
|
||||
@@ -0,0 +1,51 @@
|
||||
import json, yaml
|
||||
v2 = open(r'd:\资料\03-项目开发\wecom_it_smart_desk\docs\02-技术文档\实现配置\dify_dsl\itdesk_main_v2_result.yml', encoding='utf-8').read()
|
||||
v3 = open(r'd:\资料\03-项目开发\wecom_it_smart_desk\docs\02-技术文档\实现配置\dify_dsl\itdesk_main_v3_feedback-vars_DRAFT.yml', encoding='utf-8').read()
|
||||
|
||||
v2_top = json.loads(v2)
|
||||
v3_top = json.loads(v3)
|
||||
|
||||
print('v2 top keys:', list(v2_top.keys()))
|
||||
print('v3 top keys:', list(v3_top.keys()))
|
||||
print('v2 data len:', len(v2_top['data']))
|
||||
print('v3 data len:', len(v3_top['data']))
|
||||
|
||||
# Now load and compare inner
|
||||
v2_inner = yaml.safe_load(v2_top['data'])
|
||||
v3_inner = yaml.safe_load(v3_top['data'])
|
||||
print()
|
||||
print('v2 inner keys:', list(v2_inner.keys()))
|
||||
print('v3 inner keys:', list(v3_inner.keys()))
|
||||
|
||||
# Compare workflow structures
|
||||
v2_wf = v2_inner['workflow']
|
||||
v3_wf = v3_inner['workflow']
|
||||
print()
|
||||
print('v2 wf keys:', list(v2_wf.keys()))
|
||||
print('v3 wf keys:', list(v3_wf.keys()))
|
||||
print(f'v2 nodes: {len(v2_wf["graph"]["nodes"])} | v3 nodes: {len(v3_wf["graph"]["nodes"])}')
|
||||
print(f'v2 edges: {len(v2_wf["graph"]["edges"])} | v3 edges: {len(v3_wf["graph"]["edges"])}')
|
||||
|
||||
# Compare node counts by type
|
||||
from collections import Counter
|
||||
v2_t = Counter(n.get('data', {}).get('type') for n in v2_wf['graph']['nodes'])
|
||||
v3_t = Counter(n.get('data', {}).get('type') for n in v3_wf['graph']['nodes'])
|
||||
print()
|
||||
print('v2 types:', dict(v2_t))
|
||||
print('v3 types:', dict(v3_t))
|
||||
|
||||
# Find which nodes differ
|
||||
v2_nodes = {n['id']: n for n in v2_wf['graph']['nodes']}
|
||||
v3_nodes = {n['id']: n for n in v3_wf['graph']['nodes']}
|
||||
all_ids = set(v2_nodes) | set(v3_nodes)
|
||||
print()
|
||||
print('=== Differing nodes (data hash) ===')
|
||||
for nid in sorted(all_ids):
|
||||
v2n = v2_nodes.get(nid)
|
||||
v3n = v3_nodes.get(nid)
|
||||
h2 = hash(json.dumps(v2n, ensure_ascii=False, sort_keys=True)) if v2n else None
|
||||
h3 = hash(json.dumps(v3n, ensure_ascii=False, sort_keys=True)) if v3n else None
|
||||
if h2 != h3:
|
||||
title = (v3n or v2n)['data'].get('title', '?')
|
||||
ntype = (v3n or v2n)['data'].get('type', '?')
|
||||
print(f' {nid:30s} {ntype:18s} {title:30s} DIFFER (v2={h2}, v3={h3})')
|
||||
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
v2 = json.loads(open(r'd:\资料\03-项目开发\wecom_it_smart_desk\docs\02-技术文档\实现配置\dify_dsl\itdesk_main_v2_result.yml', encoding='utf-8').read())
|
||||
v3 = json.loads(open(r'd:\资料\03-项目开发\wecom_it_smart_desk\docs\02-技术文档\实现配置\dify_dsl\itdesk_main_v3_feedback-vars_DRAFT.yml', encoding='utf-8').read())
|
||||
|
||||
# Compare first 500 chars
|
||||
print('v2.data first 500:')
|
||||
print(v2['data'][:500])
|
||||
print()
|
||||
print('v3.data first 500:')
|
||||
print(v3['data'][:500])
|
||||
print()
|
||||
print('v2.data last 500:')
|
||||
print(v2['data'][-500:])
|
||||
print()
|
||||
print('v3.data last 500:')
|
||||
print(v3['data'][-500:])
|
||||
|
||||
# Maybe v2 has many extra fields I'm dropping? Let's see
|
||||
import yaml
|
||||
v2_inner = yaml.safe_load(v2['data'])
|
||||
v3_inner = yaml.safe_load(v3['data'])
|
||||
# Compare app field
|
||||
print()
|
||||
print('v2 app:', v2_inner.get('app'))
|
||||
print('v3 app:', v3_inner.get('app'))
|
||||
# Compare dependencies
|
||||
print()
|
||||
print('v2 deps:', v2_inner.get('dependencies'))
|
||||
print('v3 deps:', v3_inner.get('dependencies'))
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Verify the patched DSL."""
|
||||
import json, yaml
|
||||
|
||||
DST = r'd:\资料\03-项目开发\wecom_it_smart_desk\docs\02-技术文档\实现配置\dify_dsl\itdesk_main_v3_feedback-vars_DRAFT.yml'
|
||||
|
||||
raw = open(DST, encoding='utf-8').read()
|
||||
top = json.loads(raw)
|
||||
wf = yaml.safe_load(top['data'])['workflow']
|
||||
graph = wf['graph']
|
||||
|
||||
# 1. Start node
|
||||
start = next(n for n in graph['nodes'] if n['data']['type'] == 'start')
|
||||
print('=== start.variables ===')
|
||||
for v in start['data']['variables']:
|
||||
print(f" {v['variable']:20s} type={v['type']:12s} max_length={v['max_length']:5d} required={v['required']}")
|
||||
|
||||
# 2. LLM nodes — check options spec + feedback injection
|
||||
optspec = []
|
||||
fb = []
|
||||
for n in graph['nodes']:
|
||||
if n['data'].get('type') != 'llm':
|
||||
continue
|
||||
title = n['data'].get('title')
|
||||
has_opt = False
|
||||
has_fb = False
|
||||
for b in n['data'].get('prompt_template', []):
|
||||
if isinstance(b, dict):
|
||||
t = b.get('text','')
|
||||
if 'REQ-通用-005 v2.1' in t:
|
||||
has_opt = True
|
||||
if '【用户上一轮反馈(来自开始节点)】' in t:
|
||||
has_fb = True
|
||||
optspec.append((title, n['id'], has_opt))
|
||||
fb.append((title, n['id'], has_fb))
|
||||
|
||||
print()
|
||||
print('=== LLM options spec coverage ===')
|
||||
for t, i, h in optspec:
|
||||
print(f" {'[X]' if h else '[ ]'} {t:25s} ({i})")
|
||||
|
||||
print()
|
||||
print('=== LLM feedback injection coverage ===')
|
||||
for t, i, h in fb:
|
||||
print(f" {'[X]' if h else '[ ]'} {t:25s} ({i})")
|
||||
|
||||
# 3. Sample of options spec
|
||||
print()
|
||||
print('=== Sample of options spec block (tail of one LLM sys prompt) ===')
|
||||
for n in graph['nodes']:
|
||||
if n['data'].get('type') != 'llm':
|
||||
continue
|
||||
for b in n['data'].get('prompt_template', []):
|
||||
if isinstance(b, dict) and b.get('role') == 'system':
|
||||
t = b.get('text','')
|
||||
idx = t.find('REQ-通用-005 v2.1')
|
||||
if idx > 0:
|
||||
print(t[idx:])
|
||||
break
|
||||
break
|
||||
|
||||
# 4. Sample of feedback block (user prompt of main LLM)
|
||||
print()
|
||||
print('=== Sample of feedback injection (user prompt of main LLM) ===')
|
||||
for n in graph['nodes']:
|
||||
if n['data'].get('type') != 'llm' or n['id'] != '1764905844059':
|
||||
continue
|
||||
for b in n['data'].get('prompt_template', []):
|
||||
if isinstance(b, dict) and b.get('role') == 'user':
|
||||
t = b.get('text','')
|
||||
idx = t.find('【用户上一轮反馈')
|
||||
if idx > 0:
|
||||
print(t[idx:])
|
||||
break
|
||||
break
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
|
||||
# itdesk_main_v3 CHANGELOG — 反馈变量注入(最小改动版)
|
||||
|
||||
> **目的**:同时修复 Bug 1(H5 选项无稳定业务键)+ Bug 2(start 5 字段被静默丢弃),**单一动作**:
|
||||
> 让 Dify 主工作流接收 5 个新变量 + 让 AI 在生成选项时输出 `question_id` / `option_id`。
|
||||
>
|
||||
> **不动**:85 节点中除"开始"和 9 个 LLM 节点外的所有节点;所有 edges;conversation_variables;features;app 元信息。
|
||||
>
|
||||
> **风险等级**:低(仅追加文本,未删除/重写任何原有 prompt 段落)。
|
||||
>
|
||||
> **作者**:架构师(高见远 / Bob) · 2026-07-31
|
||||
|
||||
---
|
||||
|
||||
## 1. 修改摘要
|
||||
|
||||
| # | 节点 id | 节点标题 | 类型 | 修改内容 |
|
||||
|---|---------|---------|------|---------|
|
||||
| 1 | 1762235823950 | 同事咨询问题 | start | `data.variables` 从 `0 个` 增到 `5 个` |
|
||||
| 2 | 1764905844059 | 本地大模型分析 | llm | system prompt 末尾追加「REQ-通用-005 v2.1 选项输出规范」;user prompt 末尾追加「REQ-通用-005 反馈注入」 |
|
||||
| 3 | 17657866286790 | 本地大模型分析 (1) | llm | 同上(与 #2 镜像) |
|
||||
| 4 | 1765437083981 | 图片分析大模型 | llm | system prompt 末尾追加「REQ-通用-005 v2.1 选项输出规范」 |
|
||||
| 5 | 1765785630566 | 相关性判断1 | llm | 同上 |
|
||||
| 6 | 1765785673343 | 相关性判断2 | llm | 同上 |
|
||||
| 7 | 1766648935323 | 同事问题优化1 | llm | 同上 |
|
||||
| 8 | 17667264375750 | 同事问题优化2 | llm | 同上 |
|
||||
| 9 | 17669940385140 | 本地大模型分析3 | llm | 同上 |
|
||||
| 10 | 17669960812650 | 本地大模型分析3 (1) | llm | 同上 |
|
||||
|
||||
**共改动 10 个节点**(1 start + 9 llm)。其余 75 个节点(answer / assigner / code / http-request / if-else / knowledge-retrieval / list-operator / template-transform)**完全未动**。
|
||||
|
||||
---
|
||||
|
||||
## 2. start 节点新增的 5 个变量
|
||||
|
||||
`节点标题:同事咨询问题 · id: 1762235823950 · data.variables`
|
||||
|
||||
| 顺序 | variable | label | type | max_length | required |
|
||||
|------|----------|-------|------|-----------:|----------|
|
||||
| 1 | `feedback_type` | 反馈类型 | text-input | 32 | false |
|
||||
| 2 | `question_id` | 题目稳定标识 | text-input | 64 | false |
|
||||
| 3 | `option_id` | 选项稳定标识 | text-input | 64 | false |
|
||||
| 4 | `option_value` | 选项 value | text-input | 64 | false |
|
||||
| 5 | `option_label` | 选项 label(已 mask) | text-input | 200 | false |
|
||||
|
||||
**Dify 引用语法**:`{{#sys.feedback_type#}}` 等(用 `sys.` 命名空间,因为 start 节点是工作流隐式入口;本项目原 DSL 中已有的 `{{#sys.files#}}`、`{{#sys.query#}}` 即此惯例)。
|
||||
|
||||
---
|
||||
|
||||
## 3. LLM 节点 prompt 改动
|
||||
|
||||
### 3.1 system prompt 追加(9 个 LLM 节点全部追加)
|
||||
|
||||
追加到 `prompt_template[0].text` 末尾:
|
||||
|
||||
```text
|
||||
### REQ-通用-005 v2.1 选项输出规范
|
||||
|
||||
当输出结构化推荐选项时(reply_type=question_card 或带 options[] 的场景),每条 option 必须同时输出:
|
||||
- `label`:选项显示文本
|
||||
- `value`:选项 value(程序内引用)
|
||||
- `question_id`:题目稳定标识(同一题所有选项共享一个 question_id;不同时期待二次追问的选项 question_id 应当不同)
|
||||
- `option_id`:当前选项稳定标识
|
||||
|
||||
反序列化示例:
|
||||
{
|
||||
"reply_type": "question_card",
|
||||
"options": [
|
||||
{"label": "VPN 连不上", "value": "vpn_failed", "question_id": "network_diagnosis", "option_id": "vpn_failed"}
|
||||
]
|
||||
}
|
||||
|
||||
question_id/option_id 必须稳定且唯一,用于前端选项历史匹配与坐席可见性追溯。不允许只输出 label。
|
||||
```
|
||||
|
||||
> **为什么 9 个都改**:所有 LLM 节点共享同一份 Duckula 系统提示(hash 0fd8d09f37);为保证未来任意分支产出 options 都带业务键,一致性最佳。
|
||||
>
|
||||
> **为什么是"追加"而不是改"场景 2"小节**:保留原 prompt 全部原文(diff 友好),review 者在 Dify 控制台一眼能看出新增段落。
|
||||
|
||||
### 3.2 user prompt 追加(仅 2 个主 LLM 节点)
|
||||
|
||||
只对 `本地大模型分析 (1764905844059)` 和 `本地大模型分析 (1) (17657866286790)` 的 `prompt_template[1].text` 末尾追加:
|
||||
|
||||
```text
|
||||
【用户上一轮反馈(来自开始节点)】
|
||||
- feedback_type: {{#sys.feedback_type#}}
|
||||
- question_id: {{#sys.question_id#}}
|
||||
- option_id: {{#sys.option_id#}}
|
||||
- option_value: {{#sys.option_value#}}
|
||||
- option_label: {{#sys.option_label#}}
|
||||
|
||||
如果 feedback_type == "option_select",说明用户上一轮点击了 question_id={{#sys.question_id#}} 的 option_id={{#sys.option_id#}}({{#sys.option_label#}})。请据此继续追问、分诊或给出针对性建议,不要重新走首次问候流程。
|
||||
如果 feedback_type 为空,按首次提问正常处理。
|
||||
|
||||
注意:option_label 来自前端 H5 模板,已经过敏感词 mask(如手机号中间四位变成 ****),请勿尝试"还原"原值,以业务常识继续。
|
||||
```
|
||||
|
||||
> **为什么只改 2 个**:这两个 LLM 的 `text` 输出直接喂给"整合回复"answer 节点(id `1765181482474` / `17657866567850`),是真正生成给用户看的回复。其他 7 个 LLM(同事问题优化 / 相关性判断 / 本地大模型分析3)只做内部路由判断,不产出最终回复。
|
||||
>
|
||||
> **为什么用 sys 命名空间**:与项目已有惯例一致(v2 DSL 中 `{{#sys.files#}}`、`{{#sys.query#}}` 同前缀)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 操作清单(用户手动步骤)
|
||||
|
||||
### Step 0 — 备份(已完成)
|
||||
|
||||
```bash
|
||||
# 已由脚本备份:
|
||||
docs/02-技术文档/实现配置/dify_dsl/itdesk_main_2026-07-31_pre-feedback-vars_BACKUP.yml (← v2 原样)
|
||||
docs/02-技术文档/实现配置/dify_dsl/itdesk_main_v2_result.yml (← 保留不动)
|
||||
```
|
||||
|
||||
### Step 1 — 导入新 DSL 到 Dify
|
||||
|
||||
1. 打开 Dify 控制台 → 「工作室」→ 「导入 DSL」(右上角)
|
||||
2. 选择文件:`docs/02-技术文档/实现配置/dify_dsl/itdesk_main_v3_feedback-vars_DRAFT.yml`
|
||||
3. Dify 会创建**新应用**(不是覆盖原 v2 工作流);建议命名:`智能IT支持-员工咨询_v3-feedback-vars_TEST`
|
||||
4. **不要直接发布**,先在「工作室」里逐项核对(见 Step 2-4)
|
||||
|
||||
> ⚠️ Dify 的 DSL 导入是**新建应用**,原 v2 工作流不会被自动替换。如果你最终要"原地升级" v2 → v3,请用「另存为」+ 「应用 ID 不变」+ 「复制节点」手工迁移;或者直接告诉 Dify 团队在测试环境先跑 v3。
|
||||
|
||||
### Step 2 — 核对"开始"节点(截图建议:节点面板 + 变量表)
|
||||
|
||||
1. 打开新应用 → 找到 **同事咨询问题**(id `1762235823950`,最左侧 start 节点)
|
||||
2. 验证「输入字段」出现 5 个新变量:
|
||||
- 反馈类型(text-input)
|
||||
- 题目稳定标识(text-input)
|
||||
- 选项稳定标识(text-input)
|
||||
- 选项 value(text-input)
|
||||
- 选项 label(已 mask)(text-input)
|
||||
3. 全部 `required=false`;类型为 `text-input`;长度上限分别为 32/64/64/64/200
|
||||
|
||||
### Step 3 — 核对 9 个 LLM 节点的 system prompt
|
||||
|
||||
1. 依次打开下列 9 个 LLM 节点(按 Ctrl+F 搜索 title):
|
||||
- 本地大模型分析、图片分析大模型、相关性判断1、相关性判断2
|
||||
- 本地大模型分析 (1)、同事问题优化1、同事问题优化2
|
||||
- 本地大模型分析3、本地大模型分析3 (1)
|
||||
2. 滚动到 system prompt **末尾**,应看到新增小节标题:`### REQ-通用-005 v2.1 选项输出规范`
|
||||
3. 确认原有 prompt 一字未改(diff 友好)
|
||||
|
||||
### Step 4 — 核对 2 个主 LLM 节点的 user prompt
|
||||
|
||||
1. 打开 **本地大模型分析**(id `1764905844059`)
|
||||
2. 切到「用户消息」标签,滚动到末尾,应看到新增小节:`【用户上一轮反馈(来自开始节点)】`
|
||||
3. 里面应有 5 行 `{{#sys.xxx#}}` 变量引用 + 2 条 if 规则 + 1 条 mask 提醒
|
||||
4. 重复对 **本地大模型分析 (1)**(id `17657866286790`)
|
||||
|
||||
### Step 5 — 在「调试与预览」自测
|
||||
|
||||
详见 `v3_FEEDBACK_TEST_CASES.md`(3 个端到端用例)。
|
||||
|
||||
### Step 6 — 发布
|
||||
|
||||
- 在「工作室」右上角点「发布」→「发布更新」
|
||||
- 记下新应用的 **App ID**(与 v2 不同的 ID)
|
||||
- 把新 App ID 给后端团队,让他们改 Dify 调用方
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证清单
|
||||
|
||||
- [ ] Dify 控制台能正常导入 v3 DRAFT 文件,无 schema 校验错误
|
||||
- [ ] 5 个 start 变量在节点面板显示正确
|
||||
- [ ] 9 个 LLM system prompt 末尾出现新规范段
|
||||
- [ ] 2 个主 LLM user prompt 末尾出现反馈注入段
|
||||
- [ ] 调试预览中:TC-01 首次提问 AI 仍正常输出 options 且带 `question_id` / `option_id`
|
||||
- [ ] 调试预览中:TC-02 二次追问 AI 引用了 `option_label` 内容
|
||||
- [ ] 调试预览中:TC-03 跨 question_id 不串扰
|
||||
- [ ] 后端确认 `feedback_type=option_select` 的 5 字段从 start 节点传到 AI
|
||||
- [ ] H5 端确认拿到 `options[].question_id` / `options[].option_id` 后 `sendOptionSelect` 走通(这部分由 H5 团队负责,不在本次任务范围)
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键发现(架构师 review notes)
|
||||
|
||||
1. **9 个 LLM 共享同一份系统提示**(hash `0fd8d09f37`),所以"加规范"必须 9 个一起加,否则分支产出的 options 会不一致。
|
||||
2. **Dify 0.4.0 的 start 变量引用语法是 `{{#sys.xxx#}}`**,与本项目已有的 `{{#sys.files#}}`、`{{#sys.query#}}` 同前缀,**不要写成 `{{#1762235823950.feedback_type#}}`**(那种写法在某些 Dify 版本里会失败)。
|
||||
3. **"整合回复"是 answer 节点**,不是 LLM 节点;它的 `{{#1764905844059.text#}}` 直接拿上游 LLM 的 `.text` 输出。 所以 LLM 必须输出**合法 JSON**(原有 system prompt 已有此约束)—— 我没改这条规则。
|
||||
4. **user prompt 的反馈注入位置**:放在 `{{#context#}}` 之后,因为图片/图文分析路径可能不需要反馈变量;放最后最不影响原有逻辑。
|
||||
5. **没碰 `assigner` 节点**:后端把 5 字段传进 start → Dify 会自动塞到 `{{#sys.xxx#}}` → 整合回复 LLM 直接读,**不需要中间任何 assigner 中转**。这是 v3 DRAFT 比 v2 简洁的关键。
|
||||
6. **conversation 变量保持不动**(mmq / guarantee_mechanism / servyou_query / memory_ans / memory_query):原有"记忆+保底"机制继续工作,反馈注入是叠加层。
|
||||
|
||||
---
|
||||
|
||||
## 7. 已知风险 & 假设
|
||||
|
||||
- **假设 1**:Dify 0.4.0 接受 `text-input` 类型 start 变量在导入时被正确渲染为「输入字段」文本框。✓ 已与 v2 中 `{{#sys.query#}}`(text-input)行为一致。
|
||||
- **假设 2**:Dify 0.4.0 接受 system prompt 文本追加(不重排)。✓ YAML 字符串拼接是纯文本,无结构变化。
|
||||
- **假设 3**:后端 5 字段都已加在调用 Dify 的 JSON body inputs 里(属于后端 PR 范围,本任务不涉及)。
|
||||
- **风险 A**:Dify 导入时如果 schema 校验更严格,`required=false` 字段需显式 `"required": false`(已加)。如果仍报缺字段,请用 `null` 替代 `false`。
|
||||
- **风险 B**:极个别 LLM 节点(如图文判断 / 知识库分支)即使拿到反馈变量也不会在最终回答中体现——这是正常的,那些节点本来就不直接产出最终回复。
|
||||
|
||||
---
|
||||
|
||||
## 8. 不在本次范围
|
||||
|
||||
- ❌ Dify 应用的真实发布与 App ID 切换(由你/运维决定何时切流量)
|
||||
- ❌ H5 端 `sendOptionSelect` 走通(由 H5 团队负责)
|
||||
- ❌ 后端调用 Dify 时新增 5 字段(由后端团队负责)
|
||||
- ❌ Bug 3(打字机缺失,由工程团队负责)
|
||||
- ❌ AI 输出的 `options[].question_id` 命名空间约定(已有示例值 `network_diagnosis`,可按业务调整)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,328 @@
|
||||
# v3 FEEDBACK TEST CASES — 端到端验证 3 例
|
||||
|
||||
> **范围**:Dify v3 DRAFT DSL(`itdesk_main_v3_feedback-vars_DRAFT.yml`)导入后的自测。
|
||||
> **目的**:覆盖「开始节点 5 变量注入 + LLM 选项输出 question_id/option_id + 反馈注入语义」三条改动路径。
|
||||
> **执行方式**:Dify 控制台「调试与预览」 + 后端调用 Dify 服务的真实 body(inputs)。
|
||||
> **不在范围**:H5 前端 `sendOptionSelect` 走通、坐席后台可见性(属于 H5 / 后端 PR)。
|
||||
|
||||
---
|
||||
|
||||
## TC-01 首次提问(feedback_type 空)
|
||||
|
||||
### 前置条件
|
||||
- Dify 应用(v3 DRAFT 已导入并发布)已就绪,App ID 已知
|
||||
- 后端调用方把 `inputs` 字段只填 `query`(user 当前输入文本),5 个新变量**全部不传**或传 `""`
|
||||
|
||||
### 操作步骤
|
||||
|
||||
1. 打开 Dify 控制台 → 应用 → 「调试与预览」
|
||||
2. 在「输入」面板只填:
|
||||
```json
|
||||
{
|
||||
"query": "我的电脑突然很卡"
|
||||
}
|
||||
```
|
||||
3. 勾选「显示完整 JSON」/「显示原始响应」(看具体按钮名)
|
||||
4. 点「开始运行」
|
||||
|
||||
### 预期 Dify 输入 / 输出
|
||||
|
||||
**Dify 内部 start 变量值**:
|
||||
| 变量 | 值 |
|
||||
|------|-----|
|
||||
| feedback_type | `""`(空字符串) |
|
||||
| question_id | `""` |
|
||||
| option_id | `""` |
|
||||
| option_value | `""` |
|
||||
| option_label | `""` |
|
||||
|
||||
**主 LLM「本地大模型分析」(1764905844059) 收到的 user prompt**(关键片段):
|
||||
```text
|
||||
同事咨询问题:我的电脑突然很卡
|
||||
知识库内容:...
|
||||
图文信息:...
|
||||
|
||||
【用户上一轮反馈(来自开始节点)】
|
||||
- feedback_type:
|
||||
- question_id:
|
||||
- option_id:
|
||||
- option_value:
|
||||
- option_label:
|
||||
```
|
||||
|
||||
**AI 最终回复**(必须为合法 JSON,无 markdown 包裹):
|
||||
```json
|
||||
{
|
||||
"text": "电脑突然很卡?先确认下,是开机就卡还是用着用着才卡?",
|
||||
"action": null,
|
||||
"options": [
|
||||
{
|
||||
"label": "开机就卡",
|
||||
"value": "boot_slow",
|
||||
"question_id": "perf_root_cause",
|
||||
"option_id": "boot_slow"
|
||||
},
|
||||
{
|
||||
"label": "用着用着卡",
|
||||
"value": "runtime_slow",
|
||||
"question_id": "perf_root_cause",
|
||||
"option_id": "runtime_slow"
|
||||
},
|
||||
{
|
||||
"label": "不确定",
|
||||
"value": "unsure",
|
||||
"question_id": "perf_root_cause",
|
||||
"option_id": "unsure"
|
||||
}
|
||||
],
|
||||
"diagnosis_stage": "gathering_info"
|
||||
}
|
||||
```
|
||||
|
||||
### 通过标准
|
||||
|
||||
- [ ] `options` 数组里每条都同时含 `label` / `value` / `question_id` / `option_id` 4 个字段(**缺一不可**)
|
||||
- [ ] 同一题所有选项共享 `question_id`(本例都是 `perf_root_cause`)
|
||||
- [ ] 不同选项的 `option_id` 互不相同
|
||||
- [ ] `text` 简短(≤ 50 字)
|
||||
- [ ] `diagnosis_stage` 为 `gathering_info`(首次提问 → 收集信息阶段)
|
||||
- [ ] 用户消息里**不出现**「用户上一轮点击了…」类描述(因为 feedback_type 空)
|
||||
|
||||
### 失败信号
|
||||
- ❌ options 缺 question_id / option_id → LLM 没遵守新规范(system prompt 注入失败)
|
||||
- ❌ AI 问候语「您好请问…」(走首次问候流程)→ feedback_type 注入异常,但不应阻塞通过
|
||||
- ❌ 报错 "Variable feedback_type not found" → start 变量没导入成功,回到 Step 2 检查
|
||||
|
||||
---
|
||||
|
||||
## TC-02 二次追问(feedback_type=option_select)
|
||||
|
||||
### 前置条件
|
||||
- TC-01 通过
|
||||
- 后端收到 H5 端 `sendOptionSelect` 回调(用户点了「用着用着卡」),把 5 字段透传到 Dify
|
||||
|
||||
### 操作步骤
|
||||
|
||||
1. 在「调试与预览」的「输入」面板填:
|
||||
```json
|
||||
{
|
||||
"query": "用着用着卡",
|
||||
"user": "可选-用户标识",
|
||||
"feedback_type": "option_select",
|
||||
"question_id": "perf_root_cause",
|
||||
"option_id": "runtime_slow",
|
||||
"option_value": "runtime_slow",
|
||||
"option_label": "用着用着卡"
|
||||
}
|
||||
```
|
||||
2. 点「开始运行」
|
||||
|
||||
> **Dify start 变量填空规范**:Dify 0.4.0 接受 `inputs.feedback_type` 这种顶层 key 直接映射到 start 变量。如果不行,需要后端改成 `inputs: { feedback_type: "option_select", ... }` 的嵌套对象。
|
||||
|
||||
### 预期 Dify 输入 / 输出
|
||||
|
||||
**Dify 内部 start 变量值**:
|
||||
| 变量 | 值 |
|
||||
|------|-----|
|
||||
| feedback_type | `"option_select"` |
|
||||
| question_id | `"perf_root_cause"` |
|
||||
| option_id | `"runtime_slow"` |
|
||||
| option_value | `"runtime_slow"` |
|
||||
| option_label | `"用着用着卡"` |
|
||||
|
||||
**主 LLM「本地大模型分析」收到的 user prompt**(关键片段):
|
||||
```text
|
||||
同事咨询问题:用着用着卡
|
||||
知识库内容:...
|
||||
图文信息:...
|
||||
|
||||
【用户上一轮反馈(来自开始节点)】
|
||||
- feedback_type: option_select
|
||||
- question_id: perf_root_cause
|
||||
- option_id: runtime_slow
|
||||
- option_value: runtime_slow
|
||||
- option_label: 用着用着卡
|
||||
```
|
||||
|
||||
**AI 最终回复**:
|
||||
```json
|
||||
{
|
||||
"text": "用着用着才卡,多半是后台进程占资源。打开任务管理器看下 CPU/内存占用最高的进程是什么?",
|
||||
"action": null,
|
||||
"options": [
|
||||
{
|
||||
"label": "Chrome 浏览器占大头",
|
||||
"value": "chrome_heavy",
|
||||
"question_id": "perf_runtime_cause",
|
||||
"option_id": "chrome_heavy"
|
||||
},
|
||||
{
|
||||
"label": "某个办公软件卡",
|
||||
"value": "office_heavy",
|
||||
"question_id": "perf_runtime_cause",
|
||||
"option_id": "office_heavy"
|
||||
},
|
||||
{
|
||||
"label": "风扇狂转 / 温度高",
|
||||
"value": "thermal_throttle",
|
||||
"question_id": "perf_runtime_cause",
|
||||
"option_id": "thermal_throttle"
|
||||
}
|
||||
],
|
||||
"diagnosis_stage": "diagnosing"
|
||||
}
|
||||
```
|
||||
|
||||
### 通过标准
|
||||
|
||||
- [ ] `text` **明确引用**了用户的选项内容(出现「后台进程」「用着用着」等关键词)
|
||||
- [ ] `text` **不出现**「您好」「初次见面」类首次问候语
|
||||
- [ ] `text` **不出现**「您的 option_id 是 runtime_slow」这种把内部 id 念出来的串号行为
|
||||
- [ ] `diagnosis_stage` 从 `gathering_info` 推进到 `diagnosing`(二次追问,AI 已在分析)
|
||||
- [ ] 新一轮的 `options[].question_id` 必须是新值(`perf_runtime_cause` ≠ `perf_root_cause`),证明 AI 理解"在上一题基础上继续问"
|
||||
|
||||
### 失败信号
|
||||
- ❌ AI 重新走首次问候 → user prompt 里 feedback 块没被 LLM 看到(注入失败)
|
||||
- ❌ AI 完全忽略「用着用着卡」继续问「电脑怎么卡」 → 反馈信息没起到引导作用
|
||||
- ❌ 报错 "Variable sys.feedback_type not defined" → 引用语法不对,改为 `{{#1762235823950.feedback_type#}}` 重试
|
||||
|
||||
---
|
||||
|
||||
## TC-03 跨 question_id 不串扰
|
||||
|
||||
### 前置条件
|
||||
- TC-01、TC-02 通过
|
||||
- 模拟用户连点两道不同 question 的选项(模拟 H5 端历史链)
|
||||
|
||||
### 操作步骤
|
||||
|
||||
**Round 1**(首次提问,触发 TC-01)输入:
|
||||
```json
|
||||
{
|
||||
"query": "我的电脑突然很卡"
|
||||
}
|
||||
```
|
||||
→ AI 返回 question_id=`perf_root_cause` 的 3 个 options
|
||||
|
||||
**Round 2**(用户在第一题选了 `runtime_slow`,触发 TC-02)输入:
|
||||
```json
|
||||
{
|
||||
"query": "用着用着卡",
|
||||
"feedback_type": "option_select",
|
||||
"question_id": "perf_root_cause",
|
||||
"option_id": "runtime_slow",
|
||||
"option_value": "runtime_slow",
|
||||
"option_label": "用着用着卡"
|
||||
}
|
||||
```
|
||||
→ AI 返回 question_id=`perf_runtime_cause` 的新 3 个 options
|
||||
|
||||
**Round 3**(模拟:用户在第二轮后切话题,问"打印机连不上"——反馈变量保留但 question_id 切换)输入:
|
||||
```json
|
||||
{
|
||||
"query": "打印机连不上",
|
||||
"feedback_type": "option_select",
|
||||
"question_id": "perf_runtime_cause",
|
||||
"option_id": "chrome_heavy",
|
||||
"option_value": "chrome_heavy",
|
||||
"option_label": "Chrome 浏览器占大头"
|
||||
}
|
||||
```
|
||||
|
||||
### 预期 Dify 输入 / 输出
|
||||
|
||||
**主 LLM 收到的 user prompt**(关键片段):
|
||||
```text
|
||||
同事咨询问题:打印机连不上
|
||||
...
|
||||
【用户上一轮反馈(来自开始节点)】
|
||||
- feedback_type: option_select
|
||||
- question_id: perf_runtime_cause
|
||||
- option_id: chrome_heavy
|
||||
- option_value: chrome_heavy
|
||||
- option_label: Chrome 浏览器占大头
|
||||
```
|
||||
|
||||
**AI 最终回复**:
|
||||
```json
|
||||
{
|
||||
"text": "好,先放下电脑卡的问题,打印机连不上是网络打印机还是 USB 直连?",
|
||||
"action": null,
|
||||
"options": [
|
||||
{
|
||||
"label": "网络打印机",
|
||||
"value": "network_printer",
|
||||
"question_id": "printer_conn_type",
|
||||
"option_id": "network_printer"
|
||||
},
|
||||
{
|
||||
"label": "USB 直连",
|
||||
"value": "usb_printer",
|
||||
"question_id": "printer_conn_type",
|
||||
"option_id": "usb_printer"
|
||||
},
|
||||
{
|
||||
"label": "不确定",
|
||||
"value": "unsure",
|
||||
"question_id": "printer_conn_type",
|
||||
"option_id": "printer_uncertain"
|
||||
}
|
||||
],
|
||||
"diagnosis_stage": "gathering_info"
|
||||
}
|
||||
```
|
||||
|
||||
### 通过标准
|
||||
|
||||
- [ ] AI `text` **正确切话题**到打印机,**没有**继续追 Chrome 占用问题
|
||||
- [ ] AI `text` **不把"Chrome 浏览器占大头"作为当前关注点**(即不会被上轮的 option_label 误导)
|
||||
- [ ] 新一轮的 `options[].question_id = printer_conn_type` 与前两轮的 `perf_*` 系列**完全不同**
|
||||
- [ ] `diagnosis_stage` 重新回到 `gathering_info`(新一轮诊断收集信息)
|
||||
|
||||
### 失败信号
|
||||
- ❌ AI 继续追问 Chrome → 反馈注入导致 LLM 误解为"用户对 Chrome 还想继续问",说明 prompt 里需要加更强约束(属于 v3.1 优化项)
|
||||
- ❌ AI 回复里出现「既然您选了 Chrome 浏览器…」 → LLM 把"上轮选择"当成本轮输入,串扰
|
||||
- ❌ options 的 question_id 仍是 `perf_*` → AI 没切换 question 命名空间
|
||||
|
||||
---
|
||||
|
||||
## 回归检查(每次跑完 TC-01/02/03 必做)
|
||||
|
||||
- [ ] Dify 工作流**总执行时间**与 v2 持平(不因新增 user prompt 文本而显著变慢,期望差距 < 200ms)
|
||||
- [ ] conversation variables(mmq / guarantee_mechanism / servyou_query / memory_ans / memory_query)**仍按原机制更新**(不因 start 新变量被覆盖)
|
||||
- [ ] 9 个 LLM 节点**任何一个**被触发时,其 system prompt 都包含「REQ-通用-005 v2.1」字样(抽样 grep 节点 debug log)
|
||||
- [ ] 75 个非改动节点(answer / assigner / code / http-request / if-else / knowledge-retrieval / list-operator / template-transform)的 data 字段**与 v2 完全一致**(用 diff 工具跑一次)
|
||||
|
||||
---
|
||||
|
||||
## 附:Dify 调试面板 JSON 抓取脚本(Python)
|
||||
|
||||
```python
|
||||
import requests, json
|
||||
|
||||
APP_ID = "<your-v3-app-id>"
|
||||
API_KEY = "<your-app-api-key>"
|
||||
|
||||
payload = {
|
||||
"inputs": {
|
||||
"feedback_type": "option_select",
|
||||
"question_id": "perf_root_cause",
|
||||
"option_id": "runtime_slow",
|
||||
"option_value": "runtime_slow",
|
||||
"option_label": "用着用着卡"
|
||||
},
|
||||
"query": "用着用着卡",
|
||||
"user": "test-user-001",
|
||||
"response_mode": "blocking"
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
f"https://api.dify.ai/v1/chat-messages",
|
||||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||
json=payload,
|
||||
timeout=60
|
||||
)
|
||||
print(json.dumps(r.json(), ensure_ascii=False, indent=2))
|
||||
```
|
||||
|
||||
**期望** `data.answer` 字段是合法 JSON 字符串,解析后 `options[]` 每条都含 4 字段。
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user