1. 核心痛点与背景 (Core Challenges and Context)

在轻量级纯前端 Web Agent(如 InPage Agent)或无服务端会话的客户端应用中,通常会部署一个轻量级代理网关(Gateway)用于:

  1. 隐藏上游 API Key:防止用户的 API 密钥直接暴露在客户端浏览器的 Network 检查器中。
  2. 跨域与流式中继 (CORS & SSE Relay):处理浏览器同源策略,并为前端提供 SSE(Server-Sent Events)逐 Token 增量推送。

然而,单纯的 API Key 隐藏网关存在严重的安全漏洞:任何获取到网关接口地址的人,都可以直接将该网关当作免费的大模型中继(Proxy),发送任意提示词套用配额。

为了防范未授权代理滥用,同时确保客户端对动态上下文(如本地虚拟文件目录、技能注册表)的灵活注入,设计了提示词动静解耦、SHA-256 哈希签名鉴权与协议角色适配架构。


2. 核心架构设计 (Architecture Design)

flowchart TD
    subgraph Client ["前端浏览器客户端 (Frontend Agent)"]
        CSP["静态系统指令<br/>CORE_SYSTEM_PROMPT"]
        DYN["动态工作区上下文<br/>(文件树/技能表)"]
        REQ["请求体组装<br/>role: 'system' -> CORE_SYSTEM_PROMPT<br/>role: 'developer' -> 动态上下文"]
    end

    subgraph Gateway ["Go 代理网关 (Gateway Guard)"]
        HG["1. 提示词哈希校验<br/>sha256(normalize(system)) == ALLOWED_HASH ?"]
        FORBIDDEN["403 Forbidden<br/>拦截非法调用/篡改"]
        ADAPT["2. 协议角色降级适配<br/>将 role: 'developer' 合并至 system<br/>剥离 developer 消息"]
        UPSTREAM_REQ["组装上游兼容 Payload"]
    end

    subgraph Upstream ["模型供应商 (Upstream LLM Provider: DeepSeek)"]
        MODEL["安全推理 & SSE 流式输出"]
    end

    CSP --> REQ
    DYN --> REQ
    REQ -->|"POST /v1/chat/completions"| HG
    HG -- "哈希不匹配 / 无系统提示" --> FORBIDDEN
    HG -- "哈希匹配通过" --> ADAPT
    ADAPT --> UPSTREAM_REQ
    UPSTREAM_REQ -->|"Forward with API Key"| MODEL
    MODEL -->|"SSE Stream Response"| Gateway
    Gateway -->|"SSE Flush"| Client

2.1 提示词动静解耦 (Prompt Decoupling)

  • 静态核心提示词 (CORE_SYSTEM_PROMPT):固定定义在前端代码中,严格规定 Agent 的职责、工具列表与调用规范。该内容在运行时不随用户项目变化。
  • 动态上下文提示词 (developerPrompt):包含当前虚拟文件系统(VFS)的文件列表结构、当前激活的技能插件等。动态内容通过 OpenAI 规范中的 developer 角色消息传递,保持与静态 system 消息隔离。

2.2 跨平台字符串归一化与 SHA-256 校验 (Normalization & Hash Verification)

不同操作系统开发环境下的行尾换行符可能存在差异(Windows 的 \r\n 与 Linux/macOS 的 \n)。网关在计算哈希前必须进行归一化处理:

normalizedSystem := strings.TrimSpace(strings.ReplaceAll(systemPrompt, "\r\n", "\n"))
sum := sha256.Sum256([]byte(normalizedSystem))
computedHash := hex.EncodeToString(sum[:])

如果传入的 system 提示词哈希与网关环境变量 GATEWAY_SYSTEM_PROMPT_HASH 允许的哈希不符,立即返回 HTTP 403 Forbidden: {"error":{"message":"forbidden: invalid system prompt hash"}},彻底杜绝恶意篡改与未授权调用。

2.3 异构上游的角色兼容性适配 (Upstream Role Adaptation)

虽然 OpenAI 官方在 o1/GPT-4o 系列中支持 developer 角色,但许多主流开源与商业模型接口(例如 DeepSeek API)对请求体中的角色枚举进行了严格校验,接收到 role: "developer" 会直接报错抛出 HTTP 400 Bad Request: unknown variant 'developer', expected one of 'system', 'user', 'assistant', 'tool'。

网关在校验通过后执行降级合并:

  1. 提取所有 role == "developer" 消息的文本内容。
  2. 将其拼接追加至已通过校验的 role == "system" 消息末尾。
  3. 从 messages 数组中移除原 developer 项。
  4. 将净化并兼容后的 Payload 序列化并转发给上游。

3. Go 网关核心实现 (Go Gateway Implementation)

func (c Config) handleChat(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }

    body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, c.MaxBytes))
    if err != nil {
        http.Error(w, `{"error":{"message":"request body too large"}}`, http.StatusRequestEntityTooLarge)
        return
    }

    var req map[string]any
    if err := json.Unmarshal(body, &req); err != nil {
        http.Error(w, `{"error":{"message":"bad json body"}}`, http.StatusBadRequest)
        return
    }

    // 1. 核心校验:核对系统提示词 SHA-256 哈希
    if len(c.AllowedPromptHashes) > 0 {
        var systemPrompt string
        if rawMsgs, ok := req["messages"].([]any); ok {
            for _, item := range rawMsgs {
                if msgMap, ok := item.(map[string]any); ok {
                    if role, _ := msgMap["role"].(string); role == "system" {
                        systemPrompt = extractTextContent(msgMap["content"])
                        break
                    }
                }
            }
        }

        normalizedSystem := strings.TrimSpace(strings.ReplaceAll(systemPrompt, "\r\n", "\n"))
        sum := sha256.Sum256([]byte(normalizedSystem))
        computedHash := hex.EncodeToString(sum[:])

        matched := false
        for _, allowed := range c.AllowedPromptHashes {
            if strings.EqualFold(allowed, computedHash) {
                matched = true
                break
            }
        }

        if !matched {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusForbidden)
            _, _ = w.Write([]byte(`{"error":{"message":"forbidden: invalid system prompt hash"}}`))
            return
        }
    }

    // 2. 动态 Developer 消息适配上游:合并至 system 角色,消除 DeepSeek 等上游 400 报错
    if rawMsgs, ok := req["messages"].([]any); ok {
        var newMsgs []any
        var developerContents []string
        var sysMsgMap map[string]any

        for _, item := range rawMsgs {
            msgMap, ok := item.(map[string]any)
            if !ok {
                newMsgs = append(newMsgs, item)
                continue
            }
            role, _ := msgMap["role"].(string)
            if role == "developer" {
                if text := extractTextContent(msgMap["content"]); text != "" {
                    developerContents = append(developerContents, text)
                }
                continue // 剥离 developer 消息
            }
            if role <mark> "system" && sysMsgMap </mark> nil {
                sysMsgMap = msgMap
            }
            newMsgs = append(newMsgs, msgMap)
        }

        if len(developerContents) > 0 {
            joinedDev := strings.Join(developerContents, "\n\n")
            if sysMsgMap != nil {
                curr := extractTextContent(sysMsgMap["content"])
                sysMsgMap["content"] = curr + "\n\n" + joinedDev
            }
        }
        req["messages"] = newMsgs
    }

    // 3. 构造上游请求并启动流式透传
    // ...
}

4. 关键踩坑与防范规范 (Lessons & Pitfalls)

  1. 校验顺序绝不能颠倒:必须先对纯净的 CORE_SYSTEM_PROMPT 做哈希匹配,通过之后再合并 developer 内容。若先合并再计算哈希,动态文件列表和技能变更会导致每次计算出的哈希都不相同,从而破坏签名校验机制。
  2. 单双引号与环境变量解析:在 Go 实现简易 .env 加载时,必须去除值两端的包裹单/双引号,避免解析出带有字面量引号的哈希值或密钥。
  3. 多哈希兼容与灰度发布:生产网关可能需要同时支持正在运行的旧版本前端与新上线的前端,GATEWAY_SYSTEM_PROMPT_HASH 采用英文逗号分隔支持多个有效 Hash,支持平滑热升级与灰度发布。
  4. SSE 流式传输保活:网关代理上游响应时需检查 http.Flusher,在读取到上游块后立即 flusher.Flush(),杜绝缓冲区阻塞导致的客户端流式卡顿。

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps