瀏覽代碼

Clarify topic-table query generation mapping

SamLee 6 天之前
父節點
當前提交
061ea2e307

文件差異過大導致無法顯示
+ 0 - 0
app/frontend/dist/assets/index-C-ON6ep2.css


文件差異過大導致無法顯示
+ 8 - 0
app/frontend/dist/assets/index-CRU6BH8R.js


文件差異過大導致無法顯示
+ 0 - 8
app/frontend/dist/assets/index-DjAGV0Zu.js


+ 2 - 2
app/frontend/dist/index.html

@@ -4,8 +4,8 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>创作知识</title>
-    <script type="module" crossorigin src="/app/assets/index-DjAGV0Zu.js"></script>
-    <link rel="stylesheet" crossorigin href="/app/assets/index-LXPDLFMK.css">
+    <script type="module" crossorigin src="/app/assets/index-CRU6BH8R.js"></script>
+    <link rel="stylesheet" crossorigin href="/app/assets/index-C-ON6ep2.css">
   </head>
   <body>
     <div id="root"></div>

+ 1 - 1
app/frontend/src/features/query-board/QueryBoardPage.jsx

@@ -138,7 +138,7 @@ export default function QueryBoardPage() {
           ))}
         </div>
         {family.key === 'topic_table' ? (
-          <span className="refresh-state">选题字段 + LLM · 只生成 Query</span>
+          <span className="refresh-state">选题表信息 → 大模型判断 → 搜索词</span>
         ) : (
           <>
             <span className="refresh-state">

+ 121 - 134
app/frontend/src/features/query-board/TopicTableQueryPanel.jsx

@@ -4,25 +4,79 @@ import { generateTopicTableQueries, getTopicTableQueryPrompt } from '../../api/w
 const EXAMPLE = {
   topicBuildId: '1229',
   topicId: '1392',
-  maxQueries: '18',
 }
 
-const ROUTE_ORDER = [
-  'unique_hook',
-  'goal_execution',
-  'key_execution',
-  'cross_dimension',
-  'account_fit',
-  'evidence_gap',
+const ROUTES = [
+  { key: 'unique_hook', inputTitle: '灵感点和特殊组合' },
+  { key: 'goal_execution', inputTitle: '目标和想达到的效果' },
+  { key: 'key_execution', inputTitle: '关键镜头和动作要求' },
+  { key: 'cross_dimension', inputTitle: '实质、形式、意图三类元素' },
+  { key: 'account_fit', inputTitle: '账号原来的内容风格' },
+  { key: 'evidence_gap', inputTitle: '参考案例和元素来源' },
 ]
 
+function shortText(value, max = 120) {
+  const text = String(value || '').trim()
+  return text.length > max ? `${text.slice(0, max)}…` : text
+}
+
+function firstItems(values, max = 4) {
+  return [...new Set((values || []).filter(Boolean).map((value) => shortText(value)))].slice(0, max)
+}
+
+function joinNames(values, max = 8) {
+  const names = values || []
+  if (names.length <= max) return names.join('、')
+  return `${names.slice(0, max).join('、')}等`
+}
+
+function sourceItems(result, route) {
+  const topic = result.topic || {}
+  const groups = topic.input_groups || {}
+  const points = groups.points || {}
+  const dimensions = topic.dimensions || {}
+  const direction = topic.topic_direction || {}
+  const preferences = direction.persona_dimensions || {}
+
+  if (route === 'unique_hook') {
+    return firstItems(points['灵感点'] || [topic.result])
+  }
+  if (route === 'goal_execution') {
+    return firstItems([
+      ...(points['目的点'] || []),
+      dimensions['意图']?.length ? `想达到的效果:${joinNames(dimensions['意图'])}` : '',
+    ])
+  }
+  if (route === 'key_execution') {
+    return firstItems([
+      ...(points['关键点'] || []),
+      dimensions['形式']?.length ? `拍摄形式:${joinNames(dimensions['形式'])}` : '',
+    ])
+  }
+  if (route === 'cross_dimension') {
+    return firstItems([
+      dimensions['实质']?.length ? `内容里有什么:${joinNames(dimensions['实质'])}` : '',
+      dimensions['形式']?.length ? `怎么呈现:${joinNames(dimensions['形式'])}` : '',
+      dimensions['意图']?.length ? `想达到什么效果:${joinNames(dimensions['意图'])}` : '',
+    ])
+  }
+  if (route === 'account_fit') {
+    return firstItems([
+      preferences['实质偏好'] ? `常拍内容:${preferences['实质偏好']}` : '',
+      preferences['形式偏好'] ? `常用拍法:${preferences['形式偏好']}` : '',
+      preferences['意图偏好'] ? `希望观众感受到:${preferences['意图偏好']}` : '',
+    ], 3)
+  }
+  return firstItems((groups.reference_notes || []).map((row) => `${row.element}:${row.note}`), 3)
+}
+
 function PromptModal({ payload, loading, error, onClose }) {
   return (
     <div className="manual-modal-mask" onClick={onClose}>
       <section className="manual-modal-panel topic-prompt-modal" onClick={(event) => event.stopPropagation()}>
         <header className="manual-modal-head">
           <div>
-            <span className="topic-eyebrow">选题表 → Knowledge Need → Query</span>
+            <span className="topic-eyebrow">选题表信息 → 大模型判断 → 搜索词</span>
             <h2>生成 Prompt</h2>
             <p>{payload ? `${payload.name} · ${payload.version}` : '读取当前线上使用的完整 Prompt'}</p>
           </div>
@@ -45,103 +99,43 @@ function PromptModal({ payload, loading, error, onClose }) {
   )
 }
 
-function SourceCard({ result }) {
-  const topic = result.topic || {}
-  const direction = topic.topic_direction || {}
-  const dimensions = topic.dimensions || {}
-  return (
-    <section className="topic-flow-column topic-source-column">
-      <div className="topic-flow-head">
-        <span className="topic-step">01</span>
-        <div>
-          <strong>选题表提供的信息</strong>
-          <small>真实选题与账号约束</small>
-        </div>
-      </div>
-      <div className="topic-source-card">
-        <div className="topic-source-meta">
-          <span>Build {result.source?.topic_build_id}</span>
-          <span>Topic {result.source?.topic_id}</span>
-          <span>{topic.status || 'unknown'}</span>
-        </div>
-        <h3>{direction.account_name || '选题结果'}</h3>
-        <p>{topic.result}</p>
-      </div>
-      {['实质', '形式', '意图'].map((dimension) => (
-        <div className="topic-dimension-group" key={dimension}>
-          <strong>{dimension}</strong>
-          <div className="topic-chip-list">
-            {(dimensions[dimension] || []).map((name) => <span key={`${dimension}-${name}`}>{name}</span>)}
-          </div>
-        </div>
-      ))}
-      <div className="topic-point-list">
-        <strong>选题点</strong>
-        {(topic.points || []).filter((point) => point.is_active !== false).map((point) => (
-          <div key={point.id}>
-            <span>{point.point_type}</span>
-            <p>{point.point_result}</p>
-          </div>
-        ))}
-      </div>
-    </section>
-  )
-}
+function GenerationRows({ result }) {
+  const rows = useMemo(() => ROUTES.map((route, index) => ({
+    ...route,
+    number: index + 1,
+    need: (result.knowledge_needs || []).find((need) => need.route === route.key),
+    queries: (result.queries || []).filter((query) => query.route === route.key),
+    inputs: sourceItems(result, route.key),
+  })), [result])
 
-function NeedsColumn({ result }) {
-  const grouped = useMemo(() => {
-    const rows = new Map()
-    for (const need of result.knowledge_needs || []) {
-      rows.set(need.route, [...(rows.get(need.route) || []), need])
-    }
-    return rows
-  }, [result])
   return (
-    <section className="topic-flow-column topic-needs-column">
-      <div className="topic-flow-head">
-        <span className="topic-step">02</span>
-        <div>
-          <strong>LLM 判断真正未知</strong>
-          <small>六条路径形成 Knowledge Need</small>
-        </div>
+    <section className="topic-route-map">
+      <div className="topic-route-map-head">
+        <strong>选题表里用到的信息</strong>
+        <strong>大模型判断什么</strong>
+        <strong>生成哪些具体搜索词</strong>
       </div>
-      <div className="topic-needs-list">
-        {ROUTE_ORDER.flatMap((route) => grouped.get(route) || []).map((need) => (
-          <article className={`topic-need-card route-${need.route}`} key={need.need_key}>
-            <div className="topic-need-title">
-              <span>{need.route_label}</span>
-              <b>P{need.priority}</b>
+      <div className="topic-route-map-body">
+        {rows.map((row) => (
+          <article className={`topic-route-row route-${row.key}`} key={row.key}>
+            <div className="topic-route-input">
+              <span>第 {row.number} 种</span>
+              <h3>{row.inputTitle}</h3>
+              <ul>
+                {row.inputs.map((item) => <li key={item}>{item}</li>)}
+              </ul>
+            </div>
+            <div className="topic-route-judgement">
+              <p>{row.need?.decision_context || '这类信息没有形成需要判断的问题'}</p>
+            </div>
+            <div className="topic-route-queries">
+              {row.queries.map((query, index) => (
+                <div key={`${query.query}-${index}`}>
+                  <b>{index + 1}</b>
+                  <p>{query.query}</p>
+                </div>
+              ))}
             </div>
-            <p><strong>要决定:</strong>{need.decision_context}</p>
-            <p><strong>真正未知:</strong>{need.unknown_information}</p>
-          </article>
-        ))}
-      </div>
-    </section>
-  )
-}
-
-function QueryPreviewColumn({ result }) {
-  return (
-    <section className="topic-flow-column topic-query-column">
-      <div className="topic-flow-head">
-        <span className="topic-step">03</span>
-        <div>
-          <strong>原子 Query</strong>
-          <small>标准化、精确去重、预算后的结果</small>
-        </div>
-      </div>
-      <div className="topic-query-summary">
-        <span><b>{result.summary?.selected_count || 0}</b> 条 Query</span>
-        <span>{result.summary?.unique_count || 0} 条去重后</span>
-        <em>未搜索</em>
-      </div>
-      <div className="topic-query-list">
-        {(result.queries || []).map((query, index) => (
-          <article key={`${query.query}-${index}`}>
-            <span className={`topic-query-route route-${query.route}`}>{query.route_label}</span>
-            <p>{query.query}</p>
-            <small>{query.axes?.未知信息}</small>
           </article>
         ))}
       </div>
@@ -152,7 +146,6 @@ function QueryPreviewColumn({ result }) {
 export default function TopicTableQueryPanel() {
   const [topicBuildId, setTopicBuildId] = useState(EXAMPLE.topicBuildId)
   const [topicId, setTopicId] = useState(EXAMPLE.topicId)
-  const [maxQueries, setMaxQueries] = useState(EXAMPLE.maxQueries)
   const [result, setResult] = useState(null)
   const [loading, setLoading] = useState(false)
   const [error, setError] = useState('')
@@ -183,11 +176,11 @@ export default function TopicTableQueryPanel() {
       const payload = {
         topic_build_id: Number(topicBuildId),
         topic_id: topicId ? Number(topicId) : null,
-        max_queries: Number(maxQueries),
+        max_queries: 18,
       }
       setResult(await generateTopicTableQueries(payload))
     } catch (e) {
-      setError(e.message || 'Query 生成失败')
+      setError(e.message || '搜索词生成失败')
     } finally {
       setLoading(false)
     }
@@ -198,61 +191,55 @@ export default function TopicTableQueryPanel() {
       <header className="topic-table-intro">
         <div>
           <span className="topic-eyebrow">第四种生成方式</span>
-          <h2>从选题表找出真正缺少的创作知识</h2>
-          <p>读取选题字段,交给 LLM 沿六条路径判断 Knowledge Need,再拆成原子 Query。</p>
+          <h2>根据选题表生成搜索词</h2>
+          <p>每一类选题信息,都对应一项大模型判断和一组具体搜索词。</p>
         </div>
-        <span className="topic-preview-only">仅生成 Query · 不创建 Batch · 不启动搜索</span>
+        <span className="topic-preview-only">这里只生成搜索词,不会开始搜索</span>
       </header>
 
       <form className="topic-table-form" onSubmit={generate}>
         <label>
-          <span>选题构建 ID</span>
+          <span>选题表编号</span>
           <input value={topicBuildId} onChange={(e) => setTopicBuildId(e.target.value)} type="number" min="1" required />
         </label>
         <label>
-          <span>选题 ID</span>
+          <span>选题编号</span>
           <input value={topicId} onChange={(e) => setTopicId(e.target.value)} type="number" min="1" placeholder="不填则取 mature 选题" />
         </label>
-        <label>
-          <span>Query 预算</span>
-          <input value={maxQueries} onChange={(e) => setMaxQueries(e.target.value)} type="number" min="6" max="60" required />
-        </label>
         <button className="topic-example-button" type="button" onClick={() => {
           setTopicBuildId(EXAMPLE.topicBuildId)
           setTopicId(EXAMPLE.topicId)
-          setMaxQueries(EXAMPLE.maxQueries)
           setResult(null)
           setError('')
         }}>载入真实示例 1229</button>
         <button className="topic-prompt-button" type="button" onClick={showPrompt}>查看生成 Prompt</button>
         <button className="topic-generate-button" type="submit" disabled={loading}>
-          {loading ? 'LLM 正在判断需求…' : '生成 Query 预览'}
+          {loading ? '大模型正在生成…' : '生成搜索词'}
         </button>
       </form>
 
       {error && <div className="manual-error topic-generation-error">{error}</div>}
       {!result && !loading && (
-        <section className="topic-example-card">
-          <div>
-            <span>真实示例</span>
-            <strong>1229 / 1392 · 樱花树下少女与小羊互动穿搭摄影</strong>
-            <p>系统会从低角度、彩虹光晕、治愈感、好物分享等真实字段判断还缺哪些创作知识。</p>
+        <section className="topic-example-map">
+          <div className="topic-example-cell">
+            <span>第 1 种 · 灵感点和特殊组合</span>
+            <p>樱花树下低角度拍小羊,用 CCD 记录彩虹光晕</p>
+          </div>
+          <div className="topic-example-cell">
+            <span>大模型判断什么</span>
+            <p>这个画面里的彩虹光晕,怎样才能稳定拍出来?</p>
+          </div>
+          <div className="topic-example-cell">
+            <span>生成的搜索词</span>
+            <ul>
+              <li>逆光人像怎么拍出自然彩虹光晕</li>
+              <li>低角度拍摄时太阳和镜头怎么配合</li>
+            </ul>
           </div>
-          <ul>
-            <li>逆光人像怎么拍出彩虹光晕</li>
-            <li>人物和小羊互动怎么拍得自然</li>
-            <li>氛围感穿搭图文如何自然展示配饰</li>
-          </ul>
         </section>
       )}
-      {loading && <div className="topic-loading-card"><span />正在读取选题表并生成六路 Knowledge Need…</div>}
-      {result && (
-        <div className="topic-flow-grid">
-          <SourceCard result={result} />
-          <NeedsColumn result={result} />
-          <QueryPreviewColumn result={result} />
-        </div>
-      )}
+      {loading && <div className="topic-loading-card"><span />正在读取选题表,并逐类生成搜索词…</div>}
+      {result && <GenerationRows result={result} />}
       {promptOpen && (
         <PromptModal
           payload={prompt}

+ 193 - 2
app/frontend/src/styles/query-board.css

@@ -237,7 +237,7 @@
   line-height: 1.6;
 }
 
-/* Topic-table Query preview: source → six needs → atomic queries. */
+/* Topic-table search-word preview: each input stays aligned with its judgment and output. */
 .topic-table-panel {
   min-height: calc(100vh - 78px);
   padding: 18px;
@@ -291,7 +291,7 @@
 
 .topic-table-form {
   display: grid;
-  grid-template-columns: 150px 150px 120px auto auto minmax(160px, auto);
+  grid-template-columns: 150px 150px minmax(190px, auto) minmax(180px, auto) minmax(190px, auto);
   align-items: end;
   gap: 10px;
   padding: 14px;
@@ -725,6 +725,182 @@
   font-size: 12px;
 }
 
+.topic-example-map,
+.topic-route-map-head,
+.topic-route-row {
+  display: grid;
+  grid-template-columns: minmax(280px, .95fr) minmax(320px, 1fr) minmax(360px, 1.15fr);
+}
+
+.topic-example-map {
+  margin-top: 16px;
+  overflow: hidden;
+  border: 1px dashed #bdb3df;
+  border-radius: 9px;
+  background: #fff;
+}
+
+.topic-example-cell {
+  min-width: 0;
+  padding: 18px 20px;
+}
+
+.topic-example-cell + .topic-example-cell {
+  border-left: 1px solid #e7e3f1;
+}
+
+.topic-example-cell span {
+  color: #6a55bc;
+  font-size: 11px;
+  font-weight: 800;
+}
+
+.topic-example-cell p,
+.topic-example-cell li {
+  color: #403b4c;
+  font-size: 12px;
+  line-height: 1.6;
+}
+
+.topic-example-cell p {
+  margin: 8px 0 0;
+}
+
+.topic-example-cell ul {
+  margin: 7px 0 0;
+  padding-left: 18px;
+}
+
+.topic-route-map {
+  margin-top: 16px;
+  overflow: hidden;
+  border: 1px solid #ddd8e9;
+  border-radius: 9px;
+  background: #fff;
+}
+
+.topic-route-map-head {
+  border-bottom: 1px solid #ddd8e9;
+  background: #f0edf8;
+}
+
+.topic-route-map-head strong {
+  padding: 12px 18px;
+  color: #403a50;
+  font-size: 12px;
+}
+
+.topic-route-map-head strong + strong {
+  border-left: 1px solid #ddd8e9;
+}
+
+.topic-route-map-body {
+  display: grid;
+}
+
+.topic-route-row {
+  position: relative;
+  border-left: 4px solid #7c5ce5;
+}
+
+.topic-route-row + .topic-route-row {
+  border-top: 1px solid #e5e1ed;
+}
+
+.topic-route-row > div {
+  min-width: 0;
+  padding: 16px 18px;
+}
+
+.topic-route-row > div + div {
+  border-left: 1px solid #e5e1ed;
+}
+
+.topic-route-input {
+  background: #fcfbff;
+}
+
+.topic-route-input > span {
+  display: inline-block;
+  padding: 3px 7px;
+  border-radius: 5px;
+  background: #ece7fb;
+  color: #684fc1;
+  font-size: 10px;
+  font-weight: 850;
+}
+
+.topic-route-input h3 {
+  margin: 8px 0 9px;
+  color: #29243a;
+  font-size: 13px;
+}
+
+.topic-route-input ul {
+  display: grid;
+  gap: 5px;
+  margin: 0;
+  padding-left: 17px;
+}
+
+.topic-route-input li {
+  color: #686272;
+  font-size: 10px;
+  line-height: 1.5;
+}
+
+.topic-route-judgement p {
+  margin: 0;
+  color: #302b3b;
+  font-size: 12px;
+  font-weight: 650;
+  line-height: 1.6;
+}
+
+.topic-route-judgement small {
+  display: block;
+  margin-top: 8px;
+  color: #777181;
+  font-size: 10px;
+  line-height: 1.55;
+}
+
+.topic-route-queries {
+  display: grid;
+  align-content: start;
+  gap: 7px;
+}
+
+.topic-route-queries > div {
+  display: grid;
+  grid-template-columns: 20px 1fr;
+  align-items: start;
+  gap: 8px;
+  padding: 8px 10px;
+  border: 1px solid #e3dfeb;
+  border-radius: 6px;
+  background: #fff;
+}
+
+.topic-route-queries b {
+  display: grid;
+  width: 18px;
+  height: 18px;
+  place-items: center;
+  border-radius: 50%;
+  background: #eeeafb;
+  color: #6751ba;
+  font-size: 9px;
+}
+
+.topic-route-queries p {
+  margin: 0;
+  color: #292534;
+  font-size: 11px;
+  font-weight: 650;
+  line-height: 1.5;
+}
+
 @media (max-width: 1180px) {
   .topic-table-form {
     grid-template-columns: repeat(3, minmax(120px, 1fr));
@@ -734,6 +910,21 @@
     grid-template-columns: 1fr;
   }
 
+  .topic-route-map-head {
+    display: none;
+  }
+
+  .topic-example-map,
+  .topic-route-row {
+    grid-template-columns: 1fr;
+  }
+
+  .topic-example-cell + .topic-example-cell,
+  .topic-route-row > div + div {
+    border-top: 1px solid #e7e3f1;
+    border-left: 0;
+  }
+
   .topic-flow-column {
     max-height: none;
   }

+ 25 - 25
prompts/topic_table_query_generation.txt

@@ -1,42 +1,42 @@
-你是“选题表创作知识需求规划器”。输入是一条已经定稿或接近定稿的选题,以及它的账号人设、选题点、实质/形式/意图元素、元素关系和已有来源。
+你是“选题表搜索词生成助手”。输入是一条已经定稿或接近定稿的选题,以及它的账号人设、选题点、实质/形式/意图元素、元素关系和已有来源。
 
-你的任务不是复述选题,也不是判断外部事实真假,而是识别:为了把这个选题真正创作出来,创作者还缺哪些可迁移、可复用的创作知识,并把每个知识缺口拆成原子搜索 Query
+你的任务不是复述选题,也不是判断外部事实真假,而是识别:为了把这个选题真正创作出来,创作者还需要搜索哪些具体问题,并把每个问题拆成一条条可以直接搜索的短句
 
-必须从下面六条需求路径逐项检查;数据足够时,每条路径至少输出一个 Knowledge Need
+必须从下面六类信息逐项检查,每类信息输出一个需要搜索的问题
 
-1. unique_hook(独特切口落地
-   从灵感点、独特组合和高辨识度元素出发,判断这个切口真正执行时还缺什么方法
+1. unique_hook(灵感点和特殊组合
+   看灵感点、特殊组合和容易被记住的元素,判断这些画面具体要怎么拍出来
 
-2. goal_execution(创作目标实现
-   从目的点、意图和最终 result 出发,判断治愈、种草、记忆点等目标如何通过内容实现
+2. goal_execution(目标和想达到的效果
+   看目的点、意图和最终选题描述,判断治愈、种草、让人记住等效果具体怎么做出来
 
-3. key_execution(关键点执行
-   从关键点和具体形式元素出发,判断镜头、动作、结构、光线、构图等如何实际执行。
+3. key_execution(关键镜头和动作要求
+   看关键点和拍摄形式,判断镜头、动作、结构、光线、构图具体怎么执行。
 
-4. cross_dimension(跨维度组合
-   组合实质、形式和意图,判断多个元素放在同一内容中时,如何组织、取舍并保持主线清楚
+4. cross_dimension(三类元素怎么放在一起
+   把实质、形式和意图三类元素放在一起看,判断哪些应该突出、哪些应该弱化、先后怎么安排
 
-5. account_fit(账号适配
-   从 topic_direction 和 persona_dimensions 出发,判断本次选题如何延续账号已有的视觉语言、表达方式和商业目标
+5. account_fit(账号原来的内容风格
+   看账号原来常拍什么、怎么拍、想让观众产生什么感受,判断这次内容怎么保持同一种风格
 
-6. evidence_gap(来源与推导缺口
-   检查 sources、item_relations 和 reason。已有案例可能只证明某个元素有效,但没有说明迁移到当前场景后怎么做;只围绕创作方法迁移、执行失败点和画面控制形成需求
+6. evidence_gap(参考案例哪些地方不能照搬
+   看参考案例、元素关系和选择理由。旧案例换到当前场景后可能失效,需要搜索怎么调整、哪里容易拍坏
 
 判断规则:
 
-- Knowledge Need 只写“当前要做什么创作决定”和“真正未知的信息”,不要预先标成 What、How、Why;What/How/Why 是搜索内容解构后的知识类型
+- decision_context 用一句不超过 45 个汉字的大白话写“大模型要判断什么”,不要先解释背景;unknown_information 只供程序记录,用大白话写还需要查清楚什么。不要使用内部术语
 - 已经在选题表里明确决定的对象、场景和目标,不要当成未知信息重复搜索。
-- 一个 Query 只解决一个创作判断,不要把场景、穿搭、拍摄、情绪、结构全部塞进一句。
-- Query 应使用真实创作者会搜索的自然语言,避免空泛词和数据库字段名。
+- 一条搜索词只解决一个问题,不要把场景、穿搭、拍摄、情绪、结构全部塞进一句。
+- 搜索词应使用真实创作者会搜索的自然语言,避免空泛词和数据库字段名。
 - 优先搜索可迁移的创作方法、组织方式、决策依据和失败规避;不要只搜索这个具体选题标题。
-- 只生成“创作知识搜索”,不要生成政策法规、平台审核、保险报备、科学事实、行业数据、效果数值等事实取证 Query
-- 不要在 Query 中写具体账号名、年份、topic_id、item_id,也不要假设存在精确的色彩阈值或情绪数值。
+- 只生成创作方法类搜索词,不要生成政策法规、平台审核、保险报备、科学事实、行业数据、效果数值等事实取证搜索词
+- 不要在搜索词中写具体账号名、年份、topic_id、item_id,也不要假设存在精确的色彩阈值或情绪数值。
 - account_fit 要把账号偏好转译成可迁移的视觉风格、内容结构或种草方法,不要搜索具体账号自身。
 - evidence_gap 要搜索旧案例迁移到新场景时怎么调整、哪里容易失败,不要搜索外部合规规则。
-- knowledge_needs 必须正好输出 6 项:六条路径各 1 项,不多不少。不要为了凑 Query 总数,把同一路径拆成多个 Knowledge Need
-- 每个 Knowledge Need 默认产出 2~3 条互不重复的原子 Query;Query 数量放在 queries 内,不是增加 Knowledge Need 数量。
+- knowledge_needs 必须正好输出 6 项:六类信息各 1 项,不多不少。不要为了凑搜索词总数,把同一类信息拆成多个问题
+- 每个问题默认产出 2~3 条互不重复的具体搜索词;搜索词数量放在 queries 内,不是增加问题数量。
 - decision_context 只描述“要做的创作决定”;unknown_information 只概括“还缺哪类信息”。二者都不能提前编造机位厘米数、镜头参数、比例、阈值、转化率或所谓研究结论。
-- 相似 Query 可以保留,但完全重复的 Query 不要重复输出。
+- 意思相近但问法不同的搜索词可以保留,完全重复的搜索词不要重复输出。
 - source_ref 只引用输入中真实存在的 topic_build_id、topic_id、point_id、item_ids;不要编造 ID。
 - route 只能使用六个固定值之一:unique_hook、goal_execution、key_execution、cross_dimension、account_fit、evidence_gap。
 
@@ -55,13 +55,13 @@
         "item_ids": []
       },
       "queries": [
-        "只解决一个判断的原子 Query"
+        "只解决一个问题的具体搜索词"
       ]
     }
   ]
 }
 
-priority 建议范围为 0~100:独特切口和强制目标最高,关键执行和跨维度组合其次,账号适配与证据缺口根据实际风险排序。
+priority 建议范围为 0~100:灵感点和目标最高,关键镜头和三类元素组合其次,账号风格与参考案例根据实际风险排序。
 
 合格示例:
 - 逆光人像怎么拍出自然彩虹光晕

+ 30 - 14
query_planning/topic_table.py

@@ -13,15 +13,15 @@ from query_planning.service import UnifiedQueryGenerationService
 
 
 PROMPT_NAME = "topic_table_query_generation"
-PROMPT_VERSION = "topic_table_query_v1"
+PROMPT_VERSION = "topic_table_query_v2"
 
 ROUTES: dict[str, dict[str, Any]] = {
-    "unique_hook": {"label": "独特切口落地", "priority": 100},
-    "goal_execution": {"label": "创作目标实现", "priority": 90},
-    "key_execution": {"label": "关键点执行", "priority": 80},
-    "cross_dimension": {"label": "跨维度组合", "priority": 70},
-    "account_fit": {"label": "账号适配", "priority": 60},
-    "evidence_gap": {"label": "来源与推导缺口", "priority": 50},
+    "unique_hook": {"label": "灵感点和特殊组合", "priority": 100},
+    "goal_execution": {"label": "目标和想达到的效果", "priority": 90},
+    "key_execution": {"label": "关键镜头和动作要求", "priority": 80},
+    "cross_dimension": {"label": "三类元素怎么放在一起", "priority": 70},
+    "account_fit": {"label": "账号原来的内容风格", "priority": 60},
+    "evidence_gap": {"label": "参考案例哪些地方不能照搬", "priority": 50},
 }
 
 
@@ -122,9 +122,10 @@ def build_topic_table_user_prompt(
     target_query_count: int,
 ) -> str:
     return (
-        "请根据下面的真实选题表快照,沿六条固定路径识别 Knowledge Need。"
-        "knowledge_needs 数组必须正好有 6 项,每条路径正好 1 项,不能把 Query 数量误当成 Need 数量。"
-        f"六个 Need 合计生成约 {target_query_count} 条原子 Query;预算允许时,每个 Need 给 2~3 条。"
+        "请根据下面的真实选题表快照,从六类信息中各判断一个需要搜索的问题。"
+        "knowledge_needs 数组必须正好有 6 项,每类信息正好 1 项,不能把搜索词数量误当成问题数量。"
+        f"六个问题合计生成约 {target_query_count} 条具体搜索词;数量允许时,每个问题给 2~3 条。"
+        "decision_context 必须是一句不超过 45 个汉字的大白话,只说大模型要判断什么,不要先解释背景。"
         "只输出待搜索的问题,不要在 decision_context 或 unknown_information 中提前编造答案、参数、比例或效果数据。"
         "不要启动搜索。\n\n"
         + json.dumps(snapshot, ensure_ascii=False, indent=2, default=str)
@@ -143,7 +144,7 @@ def _planning_payload(
 ) -> dict[str, Any]:
     raw_needs = llm_output.get("knowledge_needs")
     if not isinstance(raw_needs, list) or not raw_needs:
-        raise TopicTableGenerationError("LLM 没有返回 knowledge_needs")
+        raise TopicTableGenerationError("大模型没有返回六类搜索问题")
     topic_build_id = snapshot["topic_build_id"]
     topic_id = snapshot["topic_id"]
     needs: list[dict[str, Any]] = []
@@ -221,10 +222,10 @@ def _planning_payload(
     ]
     if invalid_routes:
         raise TopicTableGenerationError(
-            "LLM 返回的六路 Knowledge Need 不完整或有重复:" + "、".join(invalid_routes)
+            "大模型返回的六类问题不完整或有重复:" + "、".join(invalid_routes)
         )
     if not needs or not candidates:
-        raise TopicTableGenerationError("LLM 返回内容无法形成 Knowledge Need 和 Query")
+        raise TopicTableGenerationError("大模型返回内容无法形成搜索问题和搜索词")
     return {
         "input_snapshot": snapshot,
         "knowledge_needs": needs,
@@ -254,7 +255,7 @@ def generate_topic_table_preview(
         ),
     )
     if not isinstance(llm_output, dict):
-        raise TopicTableGenerationError("LLM 返回值不是 JSON 对象")
+        raise TopicTableGenerationError("大模型返回格式不正确")
     payload = _planning_payload(llm_output, snapshot=snapshot)
     request = GenerationRequest(
         generator_kind=GeneratorKind.TOPIC_TABLE,
@@ -287,11 +288,22 @@ def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
         if route in route_counts:
             route_counts[route] += 1
     dimensions: dict[str, list[str]] = {"实质": [], "形式": [], "意图": []}
+    reference_notes: list[dict[str, str]] = []
     for item in preview.topic.get("composition_items") or []:
         dimension = item.get("dimension")
         name = item.get("element_name")
         if dimension in dimensions and name and item.get("is_active"):
             dimensions[dimension].append(name)
+        for source in item.get("sources") or []:
+            note = str(source.get("reason") or item.get("reason") or "").strip()
+            if name and note:
+                reference_notes.append({"element": str(name), "note": note})
+    point_groups: dict[str, list[str]] = {"灵感点": [], "目的点": [], "关键点": []}
+    for point in preview.topic.get("points") or []:
+        point_type = str(point.get("point_type") or "")
+        point_result = str(point.get("point_result") or "").strip()
+        if point_type in point_groups and point_result:
+            point_groups[point_type].append(point_result)
     return {
         "generator_kind": GeneratorKind.TOPIC_TABLE.value,
         "source": preview.source,
@@ -301,6 +313,10 @@ def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
             "topic_direction": preview.topic.get("topic_direction"),
             "dimensions": dimensions,
             "points": preview.topic.get("points") or [],
+            "input_groups": {
+                "points": point_groups,
+                "reference_notes": reference_notes[:8],
+            },
         },
         "knowledge_needs": [
             {

+ 7 - 1
tests/test_topic_table_query_generation.py

@@ -180,7 +180,13 @@ def test_topic_table_generation_uses_six_routes_and_unified_budget():
     assert "source_reference_data" not in seen["user"]
     assert "正好有 6 项" in seen["user"]
     assert "废弃" not in seen["user"]
-    assert "What、How、Why" in seen["system"]
+    assert "大模型要判断什么" in seen["system"]
+    assert data["topic"]["input_groups"]["points"]["灵感点"] == [
+        "低角度仰拍小羊和彩虹光晕"
+    ]
+    assert data["topic"]["input_groups"]["reference_notes"] == [
+        {"element": "低角度仰拍", "note": "草坪案例证明低角度有动感"}
+    ]
     assert data["queries"][0]["route"] == "unique_hook"
 
 

部分文件因文件數量過多而無法顯示