金融场景 Agent 设计:合规约束下的智能投研助手方案

一、"帮我查一下茅台的最近研报"——一个看似简单的请求需要多少层合规校验

金融领域的 AI Agent 和电商 Agent 有本质区别:电商出错最多是用户不满,金融出错可能涉及合规风险和被监管处罚。

一个投研助手 Agent 接到"分析茅台的投资价值"这种请求时,需要经过多层校验:

  • 用户是否是合格投资者?(合规准入)
  • 返回的信息是否包含内幕交易风险?(信息合规)
  • 生成的建议是否构成了"投资建议"?(牌照合规)
  • 数据来源是否有授权?(知识产权合规)

这些问题在通用 Agent 设计中不需要考虑,但在金融场景是生死线。

二、金融 Agent 的合规架构

三、Go 实现金融 Agent

合规校验引擎

package finance

import (
    "context"
    "fmt"
    "strings"
    "time"
)

// ComplianceLevel 合规等级
type ComplianceLevel int

const (
    CompliancePass   ComplianceLevel = 0 // 通过
    ComplianceWarn   ComplianceLevel = 1 // 警告但放行(记录审计日志)
    ComplianceBlock  ComplianceLevel = 2 // 阻止
)

// ComplianceCheck 合规检查结果
type ComplianceCheck struct {
    Level         ComplianceLevel
    Reason        string
    AuditRequired bool   // 是否需要人工复核
}

// FinancialCompliance 金融合规校验引擎
type FinancialCompliance struct {
    // 白名单:允许的查询类型
    allowedQueryTypes []string
    
    // 黑名单词:涉及内幕交易/市场操纵的关键词
    blockedKeywords []string
    
    // 禁止的输出类型——绝对不能由 Agent 生成
    forbiddenOutputs []string
}

func NewFinancialCompliance() *FinancialCompliance {
    return &FinancialCompliance{
        allowedQueryTypes: []string{
            "financial_data",     // 财务数据查询
            "research_report",    // 研报检索
            "market_analysis",    // 市场分析(不含建议)
            "risk_assessment",    // 风险评估
            "regulatory_query",   // 法规查询
            "portfolio_review",   // 持仓回顾(仅限自有持仓)
        },
        blockedKeywords: []string{
            "内幕", "操纵", "坐庄", "对倒", "老鼠仓",
            "保证收益", "稳赚", "翻倍",
        },
        forbiddenOutputs: []string{
            "投资建议", "买卖推荐", "目标价位", "建议买入", "建议卖出",
        },
    }
}

// ValidateRequest 校验用户请求
func (fc *FinancialCompliance) ValidateRequest(ctx context.Context, userID string, query string) ComplianceCheck {
    // 1. 关键词黑名单检查
    queryLower := strings.ToLower(query)
    for _, keyword := range fc.blockedKeywords {
        if strings.Contains(queryLower, keyword) {
            return ComplianceCheck{
                Level:  ComplianceBlock,
                Reason: fmt.Sprintf("请求包含敏感词: %s", keyword),
                AuditRequired: true,
            }
        }
    }
    
    // 2. 请求类型的意图检查
    if fc.containsTradingIntent(query) {
        return ComplianceCheck{
            Level:  ComplianceBlock,
            Reason: "AI 助手不能提供投资建议",
            AuditRequired: true,
        }
    }
    
    // 3. 用户资格校验
    if !fc.isQualifiedUser(ctx, userID) {
        return ComplianceCheck{
            Level:  ComplianceBlock,
            Reason: "用户不具备使用该功能的资格",
        }
    }
    
    return ComplianceCheck{
        Level:  CompliancePass,
        Reason: "合规检查通过",
    }
}

// ValidateOutput 校验 Agent 输出
func (fc *FinancialCompliance) ValidateOutput(response string) ComplianceCheck {
    // 1. 检查是否包含投资建议类输出
    for _, forbidden := range fc.forbiddenOutputs {
        if strings.Contains(response, forbidden) {
            return ComplianceCheck{
                Level:  ComplianceWarn,
                Reason: fmt.Sprintf("输出可能包含禁止内容: %s", forbidden),
                AuditRequired: true,
            }
        }
    }
    
    // 2. 检查是否包含具体股票代码 + 建议性词汇
    if fc.containsStockRecommendation(response) {
        return ComplianceCheck{
            Level:  ComplianceWarn,
            Reason: "输出包含可能被视为投资建议的内容",
            AuditRequired: true,
        }
    }
    
    return ComplianceCheck{Level: CompliancePass}
}

func (fc *FinancialCompliance) containsTradingIntent(query string) bool {
    tradingPatterns := []string{
        "能不能买", "该不该卖", "要不要持有",
        "推荐", "建议买入", "目标价",
        "明天涨", "下周跌",
    }
    for _, pattern := range tradingPatterns {
        if strings.Contains(query, pattern) {
            return true
        }
    }
    return false
}

func (fc *FinancialCompliance) isQualifiedUser(ctx context.Context, userID string) bool {
    // 实际实现中查询用户管理系统
    return true
}

func (fc *FinancialCompliance) containsStockRecommendation(response string) bool {
    // 简化版:检查 6 位数字(股票代码)+ 建议词汇的组合
    // 生产环境需要更完善的 NLP 模型
    return false
}

研报分析 Agent

// ResearchAssistant 研报助手
type ResearchAssistant struct {
    compliance   *FinancialCompliance
    dataSources  *DataSourceManager   // Wind/聚源/Bloomberg
    ragEngine    *RAGEngine           // 研报检索
    riskEngine   *RiskEngine          // 风险评估
    auditLogger  *AuditLogger         // 审计日志
}

// ResearchRequest 研报请求
type ResearchRequest struct {
    Query       string
    StockCode   string    // 股票代码
    ReportTypes []string  // 研报类型:券商/独立/行业
    DateRange   DateRange
}

// HandleResearchRequest 处理研报请求
func (ra *ResearchAssistant) HandleResearchRequest(
    ctx context.Context,
    userID string,
    req ResearchRequest,
) (string, error) {
    // ===== 第一步:合规校验 =====
    check := ra.compliance.ValidateRequest(ctx, userID, req.Query)
    if check.Level == ComplianceBlock {
        ra.auditLogger.Log(ctx, AuditEvent{
            UserID:    userID,
            Action:    "request_blocked",
            Request:   req.Query,
            Reason:    check.Reason,
            Timestamp: time.Now(),
        })
        return ra.formatComplianceReject(check), nil
    }
    
    // ===== 第二步:数据获取 =====
    // 2.1 财务数据(Wind/聚源)
    financials, err := ra.dataSources.GetFinancialData(ctx, req.StockCode)
    if err != nil {
        return "", fmt.Errorf("获取财务数据失败: %w", err)
    }
    
    // 2.2 研报检索(RAG)
    reports, err := ra.ragEngine.SearchReports(ctx, ResearchSearchQuery{
        StockCode:   req.StockCode,
        ReportTypes: req.ReportTypes,
        DateRange:   req.DateRange,
        TopK:        5,
    })
    if err != nil {
        return "", fmt.Errorf("检索研报失败: %w", err)
    }
    
    // 2.3 风险数据
    riskProfile, err := ra.riskEngine.Evaluate(ctx, req.StockCode)
    if err != nil {
        return "", fmt.Errorf("风险评估失败: %w", err)
    }
    
    // ===== 第三步:生成研报摘要 =====
    summary := ra.generateResearchSummary(ctx, financials, reports, riskProfile)
    
    // ===== 第四步:输出合规校验 =====
    outputCheck := ra.compliance.ValidateOutput(summary)
    if outputCheck.Level == ComplianceWarn {
        // 附加合规声明
        summary += ra.getComplianceDisclaimer()
    }
    
    // ===== 第五步:审计日志 =====
    ra.auditLogger.Log(ctx, AuditEvent{
        UserID:    userID,
        Action:    "research_completed",
        Request:   req.Query,
        StockCode: req.StockCode,
        Sources:   []string{"Wind", "ResearchDB", "RiskEngine"},
        Timestamp: time.Now(),
    })
    
    return summary, nil
}

func (ra *ResearchAssistant) formatComplianceReject(check ComplianceCheck) string {
    return fmt.Sprintf(
        "抱歉,根据合规要求,无法处理此请求。原因: %s",
        check.Reason,
    )
}

func (ra *ResearchAssistant) getComplianceDisclaimer() string {
    return `

---
【免责声明】
以上内容仅为信息整理,不构成任何投资建议。
所有数据来源于公开信息,AI 生成内容可能存在误差。
投资决策请基于个人判断或咨询持牌投资顾问。`
}

四、边界分析与 Trade-offs

合规校验性能

  • 每次请求的合规检查应在 50ms 内完成
  • 关键词匹配做第一层过滤(< 1ms),复杂的 NLP 语义检查通过异步方式补充

审计日志的存储

  • 金融合规要求审计日志保留至少 3-5 年
  • 日志必须包含:用户、时间、请求内容、处理结果、数据来源
  • 审计日志本身要防篡改(建议用链式哈希)

输出控制的精确度

  • "不能给投资建议"的实际边界在哪儿?分析茅台的历史走势和建议买入茅台之间的界限很细
  • 需要法务和合规部门共同定义输出红线
  • 宁可误拦也不遗漏(合规优先于体验)

免责声明的作用

  • 法律上不能完全免责,但能降低风险
  • 必须放在 AI 输出的显眼位置
  • 内容需要法务审核

五、总结

金融 Agent 设计和通用 Agent 的核心区别在于增加了合规约束层:

  1. 请求合规校验——用户资格 + 请求类型 + 关键词黑名单
  2. 数据来源合规——只能使用授权数据源,标记数据来源
  3. 输出合规校验——禁止投资建议 + 事实性声明校验 + 免责声明
  4. 全链路审计——所有请求/处理/结果全量记录,不可篡改

金融 Agent 不能为了"好用"而牺牲合规。在金融领域,合规不是附加功能,而是基础功能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐