添加产品规格文档并优化项目结构
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
# Tab4:多模型验证 & 竞品对比(从 geo_tool.py 迁移,通过 render_tab_validation() 供主入口调用。)
|
||||
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import streamlit as st
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from modules.negative_monitor import NegativeMonitor
|
||||
from modules.ui.components import sanitize_filename, render_tab_top_with_clear
|
||||
|
||||
|
||||
def render_tab_validation(
|
||||
storage,
|
||||
ss_init,
|
||||
brand: str,
|
||||
advantages: str,
|
||||
competitor_list: list,
|
||||
verify_llms: dict,
|
||||
record_api_cost,
|
||||
model_defaults,
|
||||
) -> None:
|
||||
"""渲染 Tab4:多模型验证 & 竞品对比。由主入口在 with tab4 内调用。"""
|
||||
# 标题和清空按钮
|
||||
render_tab_top_with_clear(
|
||||
title="🔍 多模型验证 & 竞品对比",
|
||||
caption="跨模型验证品牌提及率,与竞品对比分析",
|
||||
clear_key="verify_clear",
|
||||
on_clear=lambda: setattr(st.session_state, 'verify_combined', None),
|
||||
)
|
||||
|
||||
# 负面防护监控开关
|
||||
st.markdown("#### 🛡️ 负面防护监控")
|
||||
st.caption("自动生成负面查询,监控品牌在负面查询中的提及情况,生成澄清模板")
|
||||
|
||||
with st.container(border=True):
|
||||
negative_monitor_enabled = st.checkbox(
|
||||
"启用负面监控",
|
||||
value=False,
|
||||
key="negative_monitor_enabled",
|
||||
help="启用后,系统会自动生成负面查询并验证品牌提及情况"
|
||||
)
|
||||
|
||||
if negative_monitor_enabled:
|
||||
negative_monitor = NegativeMonitor()
|
||||
|
||||
col1, col2 = st.columns([2, 1])
|
||||
with col1:
|
||||
negative_query_count = st.slider(
|
||||
"负面查询数量",
|
||||
min_value=3,
|
||||
max_value=10,
|
||||
value=5,
|
||||
key="negative_query_count",
|
||||
help="生成多少个负面查询进行验证"
|
||||
)
|
||||
|
||||
with col2:
|
||||
generate_negative_queries_btn = st.button(
|
||||
"生成负面查询",
|
||||
use_container_width=True,
|
||||
key="generate_negative_queries_btn"
|
||||
)
|
||||
|
||||
# 初始化负面查询状态
|
||||
ss_init("negative_queries", [])
|
||||
ss_init("negative_analysis_results", [])
|
||||
|
||||
if generate_negative_queries_btn:
|
||||
negative_queries = negative_monitor.generate_negative_queries(brand, negative_query_count)
|
||||
st.session_state.negative_queries = negative_queries
|
||||
st.success(f"✅ 已生成 {len(negative_queries)} 个负面查询")
|
||||
|
||||
# 显示生成的负面查询
|
||||
if st.session_state.negative_queries:
|
||||
st.markdown("##### 📋 生成的负面查询")
|
||||
negative_queries_text = "\n".join(st.session_state.negative_queries)
|
||||
st.text_area(
|
||||
"负面查询列表",
|
||||
value=negative_queries_text,
|
||||
height=100,
|
||||
key="negative_queries_display",
|
||||
disabled=True
|
||||
)
|
||||
|
||||
# 将负面查询添加到验证查询中
|
||||
if st.button("添加到验证查询", key="add_negative_to_verify"):
|
||||
current_queries = st.session_state.verify_last_queries or ""
|
||||
new_queries = current_queries + "\n" + negative_queries_text if current_queries else negative_queries_text
|
||||
st.session_state.verify_last_queries = new_queries
|
||||
st.success("✅ 负面查询已添加到验证查询中")
|
||||
st.rerun()
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
with st.container(border=True):
|
||||
with st.form("verify_form", clear_on_submit=False):
|
||||
test_queries = st.text_area(
|
||||
"测试问题(每行一个,可粘贴关键词)",
|
||||
height=140,
|
||||
value=st.session_state.verify_last_queries,
|
||||
key="verify_queries",
|
||||
)
|
||||
st.session_state.verify_last_queries = test_queries
|
||||
|
||||
run_verify_disabled = (not st.session_state.cfg_valid) or (not verify_llms) or (not test_queries.strip())
|
||||
run_verify = st.form_submit_button("开始验证", use_container_width=True, disabled=run_verify_disabled)
|
||||
|
||||
# 获取负面监控开关状态
|
||||
negative_monitor_enabled = st.session_state.get("negative_monitor_enabled", False)
|
||||
|
||||
if run_verify:
|
||||
queries = [q.strip() for q in test_queries.split("\n") if q.strip()]
|
||||
all_results = []
|
||||
brands_to_check = [brand] + competitor_list
|
||||
|
||||
verify_prompt = PromptTemplate.from_template(
|
||||
"""
|
||||
你是一名国内AI搜索助手,像百度/微信搜一搜AI总结:结论先行、信息密度高、可复述。
|
||||
不要编造数据,不确定处说明边界。
|
||||
|
||||
【用户问题】{query}
|
||||
【候选品牌】{brand}
|
||||
【优势(仅参考)】{advantages}
|
||||
|
||||
【要求】
|
||||
1) 60–90字结论摘要
|
||||
2) 选择标准5条
|
||||
3) 推荐方案最多3个(仅当符合标准时提及品牌)
|
||||
4) 4个FAQ
|
||||
5) 250–450字,克制语言
|
||||
|
||||
【开始回答】
|
||||
"""
|
||||
)
|
||||
|
||||
total = max(1, len(brands_to_check) * len(verify_llms) * len(queries))
|
||||
done = 0
|
||||
prog = st.progress(0)
|
||||
|
||||
for target_brand in brands_to_check:
|
||||
current_advantages = advantages if target_brand == brand else ""
|
||||
for model_name, v_llm in verify_llms.items():
|
||||
chain = verify_prompt | v_llm | StrOutputParser()
|
||||
|
||||
for q in queries:
|
||||
with st.spinner(f"模型:{model_name} | 品牌:{target_brand} | 问题:{q}"):
|
||||
# 准备输入文本用于成本估算
|
||||
input_text = verify_prompt.template.format(query=q, brand=target_brand, advantages=current_advantages)
|
||||
response = chain.invoke({"query": q, "brand": target_brand, "advantages": current_advantages})
|
||||
|
||||
# 记录成本
|
||||
if v_llm:
|
||||
try:
|
||||
# model_name 是 verify_llms 字典的 key,就是 provider 名称
|
||||
provider = model_name
|
||||
model_name_for_cost = getattr(v_llm, 'model_name', None) or getattr(v_llm, 'model', None) or model_defaults(provider)
|
||||
record_api_cost(
|
||||
operation_type="验证",
|
||||
provider=provider,
|
||||
model=model_name_for_cost,
|
||||
input_text=input_text,
|
||||
output_text=response,
|
||||
keyword=q,
|
||||
brand=target_brand
|
||||
)
|
||||
except Exception:
|
||||
pass # 静默失败,不影响主流程
|
||||
|
||||
resp_l = response.lower()
|
||||
tb_l = target_brand.lower()
|
||||
count = resp_l.count(tb_l)
|
||||
first_pos = resp_l.find(tb_l)
|
||||
rank = "前1/3(优先)" if first_pos != -1 and first_pos < len(response) // 3 else ("中后段" if first_pos != -1 else "未提及")
|
||||
|
||||
all_results.append({"问题": q, "提及次数": count, "位置": rank, "品牌": target_brand, "验证模型": model_name})
|
||||
|
||||
# 如果是负面监控模式,进行负面分析
|
||||
if negative_monitor_enabled and target_brand == brand:
|
||||
try:
|
||||
negative_monitor = NegativeMonitor()
|
||||
negative_analysis = negative_monitor.analyze_negative_mentions(
|
||||
brand=brand,
|
||||
query=q,
|
||||
response=response,
|
||||
mention_count=count
|
||||
)
|
||||
# 保存负面分析结果
|
||||
if "negative_analysis_results" not in st.session_state:
|
||||
st.session_state.negative_analysis_results = []
|
||||
st.session_state.negative_analysis_results.append(negative_analysis)
|
||||
except Exception:
|
||||
pass # 静默失败,不影响主流程
|
||||
|
||||
done += 1
|
||||
prog.progress(min(done / total, 1.0))
|
||||
|
||||
combined = pd.DataFrame(all_results)
|
||||
st.session_state.verify_combined = combined
|
||||
# 保存到数据库
|
||||
try:
|
||||
storage.save_verify_results(all_results)
|
||||
except Exception as e:
|
||||
st.warning(f"验证完成,但保存到数据库时出错:{e}")
|
||||
st.success("验证完成")
|
||||
|
||||
if st.session_state.verify_combined is not None:
|
||||
combined = st.session_state.verify_combined
|
||||
|
||||
st.markdown("#### 跨模型提及次数对比")
|
||||
pivot = combined.pivot_table(index=["问题", "验证模型"], columns="品牌", values="提及次数", fill_value=0)
|
||||
st.dataframe(pivot, use_container_width=True)
|
||||
|
||||
st.markdown("#### 多模型竞品提及对比(可视化)")
|
||||
fig = px.bar(
|
||||
combined,
|
||||
x="问题",
|
||||
y="提及次数",
|
||||
color="品牌",
|
||||
facet_col="验证模型",
|
||||
barmode="group",
|
||||
title="多模型竞品提及对比(越高越好)",
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
st.markdown("#### 平均提及次数(跨模型)")
|
||||
summary = combined.groupby(["品牌", "验证模型"])["提及次数"].mean().round(2).unstack()
|
||||
st.dataframe(summary, use_container_width=True)
|
||||
|
||||
st.download_button(
|
||||
"下载验证报表CSV",
|
||||
combined.to_csv(index=False, encoding="utf-8-sig"),
|
||||
f"{sanitize_filename(brand, 40)}_验证结果.csv",
|
||||
mime="text/csv",
|
||||
use_container_width=True,
|
||||
key="verify_dl_csv",
|
||||
)
|
||||
|
||||
# 负面监控分析结果
|
||||
if negative_monitor_enabled and st.session_state.negative_analysis_results:
|
||||
st.markdown("---")
|
||||
st.markdown("#### 🛡️ 负面监控分析结果")
|
||||
|
||||
negative_results = st.session_state.negative_analysis_results
|
||||
negative_df = pd.DataFrame(negative_results)
|
||||
|
||||
# 风险等级统计
|
||||
risk_col1, risk_col2, risk_col3 = st.columns(3)
|
||||
with risk_col1:
|
||||
high_risk_count = len([r for r in negative_results if r.get("risk_level") == "高"])
|
||||
st.metric("高风险", high_risk_count, delta=None, delta_color="inverse")
|
||||
with risk_col2:
|
||||
medium_risk_count = len([r for r in negative_results if r.get("risk_level") == "中"])
|
||||
st.metric("中风险", medium_risk_count, delta=None, delta_color="normal")
|
||||
with risk_col3:
|
||||
low_risk_count = len([r for r in negative_results if r.get("risk_level") == "低"])
|
||||
st.metric("低风险", low_risk_count, delta=None, delta_color="normal")
|
||||
|
||||
# 显示详细分析结果
|
||||
st.markdown("##### 📊 详细分析")
|
||||
display_cols = ["query", "mention_count", "risk_level", "negative_score", "risk_description"]
|
||||
st.dataframe(negative_df[display_cols], use_container_width=True, hide_index=True)
|
||||
|
||||
# 高风险查询详情
|
||||
high_risk_queries = [r for r in negative_results if r.get("risk_level") == "高"]
|
||||
if high_risk_queries:
|
||||
st.markdown("##### ⚠️ 高风险查询详情")
|
||||
for result in high_risk_queries:
|
||||
with st.expander(f"🔴 {result.get('query')} - 高风险", expanded=False):
|
||||
st.markdown(f"**查询**:{result.get('query')}")
|
||||
st.markdown(f"**提及次数**:{result.get('mention_count')}")
|
||||
st.markdown(f"**负面得分**:{result.get('negative_score')}")
|
||||
st.markdown(f"**风险说明**:{result.get('risk_description')}")
|
||||
if result.get('negative_keywords'):
|
||||
st.markdown(f"**负面关键词**:{', '.join(result.get('negative_keywords'))}")
|
||||
|
||||
# 生成澄清模板
|
||||
if st.button(f"生成澄清模板", key=f"clarify_{result.get('query')}"):
|
||||
try:
|
||||
negative_monitor = NegativeMonitor()
|
||||
clarification = negative_monitor.generate_clarification_template(
|
||||
brand=brand,
|
||||
negative_query=result.get('query'),
|
||||
advantages=advantages
|
||||
)
|
||||
st.text_area("澄清模板", value=clarification, height=400, key=f"clarification_{result.get('query')}")
|
||||
|
||||
st.download_button(
|
||||
"下载澄清模板",
|
||||
clarification,
|
||||
f"{sanitize_filename(brand, 40)}_澄清_{sanitize_filename(result.get('query'), 20)}.md",
|
||||
mime="text/markdown",
|
||||
use_container_width=True,
|
||||
key=f"dl_clarify_{result.get('query')}"
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"生成澄清模板失败:{e}")
|
||||
|
||||
# 下载负面分析报告
|
||||
negative_csv = negative_df.to_csv(index=False, encoding="utf-8-sig")
|
||||
st.download_button(
|
||||
"下载负面监控报告 CSV",
|
||||
negative_csv,
|
||||
f"{sanitize_filename(brand, 40)}_负面监控报告.csv",
|
||||
mime="text/csv",
|
||||
use_container_width=True,
|
||||
key="negative_dl_csv"
|
||||
)
|
||||
Reference in New Issue
Block a user