feat: 初始提交 - 科创企业特有风险的识别与管理 (数智风控系统)

This commit is contained in:
Chen Xiao
2026-08-14 08:21:26 +08:00
commit a41576a8c0
190 changed files with 28531 additions and 0 deletions
@@ -0,0 +1,540 @@
# -*- coding: utf-8 -*-
"""
⚖️ 多智能体辩论诊断页面 (Premium Financial-Grade Design)
选择企业 → 真实 Token 级打字机流式推演 → 三方交叉质证 → 综合裁决与核保看板
使用 Glassmorphism 玻璃拟态 + 暗黑金融科技 CSS 调色盘
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import streamlit as st
import plotly.graph_objects as go
import time
from collectors.financial_collector import get_all_companies, get_company_by_code
from agents.debate_engine import DebateEngine
st.set_page_config(page_title="多智能体辩论诊断", page_icon="⚖️", layout="wide")
# ============================================================
# 自定义 UI 视觉增强样式 (CSS 注入)
# ============================================================
st.markdown("""
<style>
/* 引入 Google 科技字体 */
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&family=Inter:wght@400;600;700&display=swap');
/* 顶栏 Hero 区域 */
.hero-banner {
background: linear-gradient(135deg, rgba(26, 26, 46, 0.95), rgba(22, 33, 62, 0.9), rgba(15, 52, 96, 0.95));
border: 1px solid rgba(233, 69, 96, 0.25);
border-radius: 16px;
padding: 24px 30px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
backdrop-filter: blur(12px);
}
.hero-title {
color: #FFFFFF;
font-size: 2.0rem;
font-weight: 700;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 12px;
}
.hero-subtitle {
color: #94A3B8;
font-size: 1.0rem;
margin: 0;
}
.step-badge-container {
display: flex;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap;
}
.step-badge {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #CBD5E1;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.82rem;
font-weight: 500;
}
.step-badge-active {
background: rgba(233, 69, 96, 0.15);
border-color: #e94560;
color: #ff758c;
}
/* 智能体研判卡片 (Glassmorphism) */
.agent-card {
background: rgba(22, 33, 62, 0.7);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
transition: all 0.3s ease;
height: 100%;
}
.agent-card:hover {
border-color: rgba(233, 69, 96, 0.4);
transform: translateY(-2px);
}
.agent-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding-bottom: 12px;
margin-bottom: 16px;
}
.agent-name {
font-size: 1.15rem;
font-weight: 700;
color: #F8FAFC;
}
/* 风险指标 Badge */
.risk-pill {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.78rem;
font-weight: 600;
margin-left: 6px;
}
.pill-high { background: rgba(244, 67, 54, 0.2); color: #ff6b6b; border: 1px solid rgba(244, 67, 54, 0.4); }
.pill-med { background: rgba(255, 152, 0, 0.2); color: #ffb74d; border: 1px solid rgba(255, 152, 0, 0.4); }
.pill-low { background: rgba(76, 175, 80, 0.2); color: #81c784; border: 1px solid rgba(76, 175, 80, 0.4); }
/* 模拟黑客流式终端 (Terminal Box) */
.terminal-box {
background: #0B0E14;
border: 1px solid #1E293B;
border-radius: 10px;
padding: 14px;
font-family: 'Fira Code', monospace;
font-size: 0.84rem;
color: #38BDF8;
line-height: 1.5;
overflow-x: auto;
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8);
}
.terminal-thinking {
color: #94A3B8;
font-style: italic;
border-left: 2px solid #6366F1;
padding-left: 10px;
margin-bottom: 10px;
}
.terminal-output {
color: #4ADE80;
border-left: 2px solid #22C55E;
padding-left: 10px;
}
/* 裁决结果看板 */
.verdict-banner {
background: linear-gradient(135deg, #1E1B4B, #31103F);
border: 1px solid #6366F1;
border-radius: 14px;
padding: 24px;
text-align: center;
margin-top: 16px;
}
.verdict-title {
font-size: 1.8rem;
font-weight: 800;
letter-spacing: 1px;
}
</style>
""", unsafe_allow_html=True)
# ============================================================
# 页面顶部 Banner
# ============================================================
st.markdown("""
<div class="hero-banner">
<div class="hero-title">
<span>⚖️</span>
<span>多智能体交叉验证辩论诊断</span>
</div>
<div class="hero-subtitle">
基于 Multi-Agent 辩论图谱 · 法务 / 技术 / 财务专家分布式研判 · 全过程打字机流式追溯
</div>
<div class="step-badge-container">
<span class="step-badge step-badge-active">Phase 1: 独立穿透研判</span>
<span class="step-badge">Phase 2: 三方交叉质证</span>
<span class="step-badge">Phase 3: 委员会综合裁决</span>
<span class="step-badge">🛡️ 保险精算核保建议</span>
</div>
</div>
""", unsafe_allow_html=True)
from utils.session_helper import render_company_selector, render_sidebar_global_company_selector
with st.sidebar:
render_sidebar_global_company_selector()
st.markdown("---")
col_ctrl1, col_ctrl2 = st.columns([3, 1])
with col_ctrl1:
company = render_company_selector("🏢 选择待诊断科创企业", key_suffix="debate_page")
stock_code = company["stock_code"] if company else "688256"
# 初始化 session_state
if "debate_results" not in st.session_state:
st.session_state["debate_results"] = None
with col_ctrl2:
st.markdown("<div style='height: 28px;'></div>", unsafe_allow_html=True)
start_btn = st.button("🚀 启动多智能体辩论", type="primary", use_container_width=True)
# ============================================================
# 辩论触发与打字机流式渲染
# ============================================================
if start_btn:
company = get_company_by_code(stock_code)
if not company:
st.error("未找到企业数据")
st.stop()
engine = DebateEngine()
company_name = company.get("short_name", "未知")
st.markdown("---")
st.markdown("### 📡 智能体实时思考与流式推演控制台 (Live Streaming Terminal)")
progress_bar = st.progress(0, text="▶️ 正在初始化分布式智能体网络...")
# 动态渲染控制台
live_box = st.container(border=True)
def run_agent_with_typewriter(agent, agent_title, start_progress, end_progress):
progress_bar.progress(start_progress, text=f"{agent.role_icon} {agent_title} 正在穿透企业数据并与大模型通信...")
with live_box:
st.markdown(f"##### {agent.role_icon} {agent_title}")
status_placeholder = st.empty()
thinking_placeholder = st.empty()
content_placeholder = st.empty()
thinking_buf = []
content_buf = []
status_placeholder.info("🔗 建立 SSE 流式通信中...")
def on_token(token_type: str, token_text: str):
if token_type == "reasoning":
thinking_buf.append(token_text)
t_str = "".join(thinking_buf)
status_placeholder.markdown("🧠 **[大模型深度思考中...]**")
display_text = t_str[-350:] if len(t_str) > 350 else t_str
thinking_placeholder.markdown(
f"""<div class="terminal-box terminal-thinking">
<b>[THINKING]</b> {display_text}
</div>""",
unsafe_allow_html=True
)
elif token_type == "content":
content_buf.append(token_text)
c_str = "".join(content_buf)
status_placeholder.markdown("📝 **[正在实时生成审查报告...]**")
display_c = c_str[-280:] if len(c_str) > 280 else c_str
content_placeholder.markdown(
f"""<div class="terminal-box terminal-output">
<b>[REPORT OUTPUT]</b> {display_c}
</div>""",
unsafe_allow_html=True
)
agent.on_token_callback = on_token
result = agent.evaluate(company if agent != engine.judge_agent else None)
progress_bar.progress(end_progress, text=f"{agent_title} 研判完毕")
status_placeholder.success(f"{agent_title} 完成风险评级")
return result, list(agent.reasoning_trace)
# 1. 法务 Agent
law_result, law_trace = run_agent_with_typewriter(engine.law_agent, "法务风控节点", 10, 35)
# 2. 技术 Agent
tech_result, tech_trace = run_agent_with_typewriter(engine.tech_agent, "技术风控节点", 35, 60)
# 3. 财务 Agent
finance_result, finance_trace = run_agent_with_typewriter(engine.finance_agent, "财务风控节点", 60, 85)
# 4. 交叉质证
progress_bar.progress(88, text="🔄 正在比对三方判定结论,进行交叉质证分析...")
conflicts = engine._identify_conflicts(law_result, tech_result, finance_result)
# 5. 综合裁决 Agent
progress_bar.progress(92, text="⚖️ 综合裁决委员会进行权重复核...")
with live_box:
st.markdown("##### ⚖️ 综合裁决节点")
j_status = st.empty()
j_thinking = st.empty()
j_content = st.empty()
j_think_buf = []
j_cont_buf = []
j_status.info("🚀 综合裁决委员会正在消解分歧...")
def judge_on_token(token_type: str, token_text: str):
if token_type == "reasoning":
j_think_buf.append(token_text)
t_str = "".join(j_think_buf)
j_status.markdown("🧠 **[裁决委员会讨论中...]**")
display_t = t_str[-350:] if len(t_str) > 350 else t_str
j_thinking.markdown(
f"""<div class="terminal-box terminal-thinking">
<b>[COMMITTEE THOUGHTS]</b> {display_t}
</div>""",
unsafe_allow_html=True
)
elif token_type == "content":
j_cont_buf.append(token_text)
c_str = "".join(j_cont_buf)
j_status.markdown("📝 **[生成核保决议中...]**")
display_c = c_str[-280:] if len(c_str) > 280 else c_str
j_content.markdown(
f"""<div class="terminal-box terminal-output">
<b>[FINAL VERDICT]</b> {display_c}
</div>""",
unsafe_allow_html=True
)
engine.judge_agent.on_token_callback = judge_on_token
judge_result = engine.judge_agent.evaluate(company, law_result, tech_result, finance_result)
judge_trace = list(engine.judge_agent.reasoning_trace)
j_status.success("✅ 综合裁决完成")
progress_bar.progress(100, text="✅ 辩论诊断流程全量完成!")
# 持久化结果
st.session_state["debate_results"] = {
"company": company,
"law_result": law_result,
"tech_result": tech_result,
"finance_result": finance_result,
"law_trace": law_trace,
"tech_trace": tech_trace,
"finance_trace": finance_trace,
"judge_trace": judge_trace,
"conflicts": conflicts,
"judge_result": judge_result,
"debate_log": engine.debate_log,
}
# ============================================================
# 结果持久化渲染与面板展示
# ============================================================
def render_trace_expander(trace: list, title: str):
"""渲染研判结果对应的详细辩论链"""
with st.expander(f"🔗 查看 【{title}】 详细辩论链与凭据 (共 {len(trace)} 步)", expanded=False):
for step in trace:
ts = step.get("timestamp", "")
step_name = step.get("step", "")
content = step.get("content", "")
if "思维链" in step_name or "原始" in step_name or "Context" in step_name or len(content) > 150:
st.markdown(f"**`[{ts}]` {step_name}**")
st.code(content, language="text")
else:
st.markdown(f"- **`[{ts}]` {step_name}**: {content}")
def render_risk_card(result: dict, keys: list, trace: list, agent_title: str, overall_key: str):
"""渲染三方风控精美卡片"""
overall = result.get(overall_key, {}).get("score", 50)
level = result.get(overall_key, {}).get("level", "")
pill_class = "pill-high" if level == "" else ("pill-med" if level == "" else "pill-low")
st.markdown(f"""
<div class="agent-card">
<div class="agent-header">
<span class="agent-name">{agent_title}</span>
<span><span class="risk-pill {pill_class}">{level}风险</span> <b>{overall}分</b></span>
</div>
""", unsafe_allow_html=True)
# 细分指标渲染
for key in keys:
item = result.get(key, {})
if isinstance(item, dict) and "detail" in item:
score = item.get("score", 0)
color = "#EF4444" if score >= 70 else ("#F59E0B" if score >= 40 else "#10B981")
st.markdown(
f"<div style='padding:8px 12px; border-radius:6px; margin:6px 0; "
f"border-left:4px solid {color}; background:rgba(255,255,255,0.03); font-size:0.88rem;'>"
f"<b style='color:{color};'>{score}分</b> — {item.get('detail', '')}</div>",
unsafe_allow_html=True,
)
# 关键发现
findings = result.get("key_findings", [])
if findings:
st.markdown("<p style='font-size:0.85rem; font-weight:700; color:#94A3B8; margin-top:12px; margin-bottom:4px;'>📌 关键风险发现:</p>", unsafe_allow_html=True)
for f in findings:
st.markdown(f"<span style='font-size:0.83rem; color:#E2E8F0;'>• {f}</span>", unsafe_allow_html=True)
# 建议
recs = result.get("recommendations", [])
if recs:
st.markdown("<p style='font-size:0.85rem; font-weight:700; color:#94A3B8; margin-top:10px; margin-bottom:4px;'>💡 专家处置建议:</p>", unsafe_allow_html=True)
for r in recs:
st.markdown(f"<span style='font-size:0.83rem; color:#CBD5E1;'>• {r}</span>", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
render_trace_expander(trace, agent_title)
res = st.session_state.get("debate_results")
if res is not None:
company = res["company"]
law_result = res["law_result"]
tech_result = res["tech_result"]
finance_result = res["finance_result"]
conflicts = res["conflicts"]
judge_result = res["judge_result"]
st.markdown("---")
st.markdown(f"### 📊 多智能体交叉辩论诊断报告 — {company['short_name']} ({company['stock_code']})")
# ---- Phase 1: 三方独立研判 ----
st.markdown("#### 📋 Phase 1: 三方专家节点穿透研判全景")
col_law, col_tech, col_fin = st.columns(3)
with col_law:
render_risk_card(
law_result,
["algo_compliance_risk", "geopolitical_risk", "data_compliance_risk", "ip_litigation_risk"],
res.get("law_trace", []),
"👩‍⚖️ 法务风控节点",
"overall_law_risk",
)
with col_tech:
render_risk_card(
tech_result,
["tech_disruption_risk", "talent_loss_risk", "patent_moat", "tech_iteration_pressure"],
res.get("tech_trace", []),
"👨‍🔬 技术风控节点",
"overall_tech_risk",
)
with col_fin:
render_risk_card(
finance_result,
["rd_capitalization_risk", "concentration_risk", "receivable_risk", "cashflow_risk"],
res.get("finance_trace", []),
"👔 财务风控节点",
"overall_fin_risk",
)
# ---- Phase 2: 交叉质证分析 ----
st.markdown("---")
st.markdown("#### 🔄 Phase 2: 三方交叉质证与冲突判定")
if conflicts:
for conflict in conflicts:
st.markdown(f"""
<div style='background:rgba(239, 68, 68, 0.1); border:1px solid rgba(239, 68, 68, 0.3); border-radius:8px; padding:12px 16px; margin:6px 0; color:#FCA5A5;'>
⚠️ <b>判定冲突警示</b>{conflict}
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div style='background:rgba(16, 185, 129, 0.1); border:1px solid rgba(16, 185, 129, 0.3); border-radius:8px; padding:12px 16px; color:#6EE7B7;'>
✅ <b>一致性确认</b>:法务、技术、财务三方判定逻辑高度契合,无重大矛盾分歧。
</div>
""", unsafe_allow_html=True)
# ---- Phase 3: 综合裁决与保险核保 ----
st.markdown("---")
st.markdown("#### ⚖️ Phase 3: 风险委员会综合裁决与核保决策")
comp_score = judge_result.get("comprehensive_score", 50)
decision = judge_result.get("underwriting_decision", "标准承保")
risk_level = judge_result.get("risk_level", "")
col_j1, col_j2 = st.columns([1, 1])
with col_j1:
# Plotly 仪表盘 (暗黑高精质感)
fig = go.Figure(go.Indicator(
mode="gauge+number",
value=comp_score,
title={"text": "综合风险指数 (0-100)", "font": {"color": "#F8FAFC", "size": 16}},
number={"font": {"color": "#FFFFFF", "size": 48}},
gauge={
"axis": {"range": [0, 100], "tickcolor": "#94A3B8"},
"bar": {"color": "#e94560", "width": 0.3},
"bgcolor": "rgba(0,0,0,0)",
"bordercolor": "rgba(255,255,255,0.1)",
"steps": [
{"range": [0, 40], "color": "rgba(16, 185, 129, 0.25)"},
{"range": [40, 60], "color": "rgba(245, 158, 11, 0.25)"},
{"range": [60, 80], "color": "rgba(249, 115, 22, 0.25)"},
{"range": [80, 100], "color": "rgba(239, 68, 68, 0.25)"},
],
},
))
fig.update_layout(
height=260,
margin=dict(l=20, r=20, t=40, b=10),
paper_bgcolor="rgba(0,0,0,0)",
font=dict(color="white"),
)
st.plotly_chart(fig, use_container_width=True)
with col_j2:
if "拒绝" in decision:
v_color = "#EF4444"
v_bg = "rgba(239, 68, 68, 0.15)"
elif "附条件" in decision:
v_color = "#F59E0B"
v_bg = "rgba(245, 158, 11, 0.15)"
elif "优先" in decision:
v_color = "#10B981"
v_bg = "rgba(16, 185, 129, 0.15)"
else:
v_color = "#3B82F6"
v_bg = "rgba(59, 130, 246, 0.15)"
st.markdown(f"""
<div class="verdict-banner" style="border-color:{v_color}; background:{v_bg};">
<div class="verdict-title" style="color:{v_color};">核保决策: 【{decision}】</div>
<p style="color:#CBD5E1; margin-top:8px;">综合风险等级: <b>{risk_level}</b> | 加权裁决得分: <b>{comp_score}分</b></p>
</div>
""", unsafe_allow_html=True)
st.markdown(f"**📝 裁决终审意见**: {judge_result.get('summary', '')}")
conditions = judge_result.get("underwriting_conditions", [])
if conditions:
st.markdown("**📌 保险核保附加条件**:")
for c in conditions:
st.markdown(f"- <span style='color:#F59E0B;'>{c}</span>", unsafe_allow_html=True)
# 挂载裁决节点的辩论链
st.markdown("<br>", unsafe_allow_html=True)
render_trace_expander(res.get("judge_trace", []), "⚖️ 综合裁决委员会")
else:
# 默认引导说明
st.markdown("""
<div style='background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.08); border-radius:12px; padding:24px; margin-top:20px;'>
<h4 style='color:#F8FAFC; margin-top:0;'>💡 多智能体辩论与风险识别流程说明</h4>
<ol style='color:#94A3B8; line-height:1.8;'>
<li><b>Phase 1 - 三方穿透研判</b>: 法务风控节点审查算法与出口管制、技术风控节点审查路线与人员、财务风控节点穿透资本化与集中度。</li>
<li><b>Phase 2 - 交叉质证分析</b>: 识别法务、技术、财务意见间的潜在分歧(如:研发投入大 vs 资本化美化利润)。</li>
<li><b>Phase 3 - 委员会综合裁决</b>: 基于【合规 > 技术 > 财务】优先级规则进行加权综合评分并输出精算核保决议。</li>
</ol>
</div>
""", unsafe_allow_html=True)