一、“伪私有化”的四种典型形态

“伪私有化”不是指厂商故意欺骗,而是“私有化”的定义被模糊化了。常见的四种形态:

形态一:管理面私有化,推理面公有云。 控制台、数据库、日志部署在企业内网,但模型推理通过HTTPS调用厂商云端的API。数据“看起来”在内网,实际上每次推理都经过外部服务器。

形态二:本地客户端 + 云端模型。 企业内网部署一个客户端或网关,但网关的转发目标是外部API。客户端只是“代理”,推理仍在云上。

形态三:模型权重本地部署,但依赖云端鉴权/遥测。 模型文件下载到本地,推理确实在本地跑,但每次启动或调用都需要向厂商服务器请求鉴权令牌,或回传使用遥测数据。断网后无法运行。

形态四:混合推理,敏感数据外调。 大部分推理在本地,但涉及“复杂任务”时自动降级到云端模型。数据在本地和云端之间流动,边界不可控。

这四种形态的共同特征是:企业以为数据在内网,实际上存在一条未经确认的出站链路。 甄别的核心,是找到这条链路。

二、架构差异:本地推理与API外调的四个分水岭

┌─────────────────────────────────────────────────────────────────┐
│                    本地推理架构                                  │
│                                                                  │
│  应用层 → 推理服务(内网GPU)→ 模型权重(本地文件)              │
│              │                                                   │
│              ├── 鉴权:本地密钥/无鉴权                            │
│              ├── 数据:全程内网,无出站                           │
│              └── 审计:本地日志,可全量导出                       │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    API外调架构(伪私有化)                       │
│                                                                  │
│  应用层 → 本地网关 → HTTPS出站 → 厂商云端API → 模型推理          │
│              │                        │                          │
│              ├── 鉴权:厂商API Key    ├── 数据经过外部服务器      │
│              ├── 数据:请求/响应出站  └── 审计:厂商侧日志        │
│              └── 依赖:断网即不可用                               │
└─────────────────────────────────────────────────────────────────┘

分水岭一:网络出站。 本地推理的推理服务只监听内网地址,不发起任何外部连接。API外调的网关必然有出站HTTPS请求。

分水岭二:鉴权依赖。 本地推理的鉴权在本地完成(或无需鉴权),API外调需要向厂商服务器请求令牌或携带厂商API Key。

分水岭三:数据流向。 本地推理的请求和响应全程在内网内存中流转,API外调的请求体(含Prompt)和响应体(含生成内容)经过外部网络。

分水岭四:断网可用性。 本地推理断网后可正常运行(模型权重已加载),API外调断网后推理完全不可用。

三、网络层甄别:抓包与出站管控

最直接的甄别方法是网络层观测。

3.1 出站连接监控

import psutil
import socket
from dataclasses import dataclass
from typing import List

@dataclass
class OutboundConnection:
    """出站连接记录"""
    pid: int
    process_name: str
    local_addr: str
    remote_addr: str
    remote_port: int
    protocol: str
    is_external: bool

class OutboundMonitor:
    """出站连接监控器:识别AI服务的对外连接"""
    
    # 常见的大模型API域名前缀
    LLM_API_PATTERNS = [
        "api.openai.com", "api.anthropic.com",
        "dashscope.aliyuncs.com", "aip.baidubce.com",
        "ark.cn-beijing.volces.com", "api.moonshot.cn",
        "open.bigmodel.cn", "api.deepseek.com"
    ]
    
    def scan(self) -> List[OutboundConnection]:
        """扫描当前系统的出站连接"""
        connections = []
        for conn in psutil.net_connections(kind="inet"):
            if conn.status != "ESTABLISHED" or not conn.raddr:
                continue
            
            remote_ip, remote_port = conn.raddr
            # 判断是否外部地址
            is_external = not self._is_private_ip(remote_ip)
            
            try:
                proc = psutil.Process(conn.pid)
                proc_name = proc.name()
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                proc_name = "unknown"
            
            connections.append(OutboundConnection(
                pid=conn.pid,
                process_name=proc_name,
                local_addr=f"{conn.laddr.ip}:{conn.laddr.port}",
                remote_addr=remote_ip,
                remote_port=remote_port,
                protocol="TCP",
                is_external=is_external
            ))
        
        return [c for c in connections if c.is_external]
    
    def check_llm_api_calls(self) -> List[dict]:
        """检测是否在调用已知的大模型API"""
        suspicious = []
        for conn in self.scan():
            # 反向解析域名(或查DNS缓存)
            hostname = self._reverse_dns(conn.remote_addr)
            for pattern in self.LLM_API_PATTERNS:
                if hostname and pattern in hostname:
                    suspicious.append({
                        "process": conn.process_name,
                        "pid": conn.pid,
                        "target": hostname,
                        "port": conn.remote_port,
                        "risk": "external_llm_api"
                    })
        return suspicious
    
    def _is_private_ip(self, ip: str) -> bool:
        """判断是否为内网地址"""
        import ipaddress
        try:
            return ipaddress.ip_address(ip).is_private
        except ValueError:
            return False
    
    def _reverse_dns(self, ip: str) -> str:
        """反向DNS解析"""
        try:
            return socket.gethostbyaddr(ip)[0]
        except (socket.herror, socket.gaierror):
            return ""

甄别操作:在POC环境中,用出站监控工具(如ss -tp、netstat或上述脚本)观察AI服务的网络连接。如果发现推理进程持续向外部地址发起HTTPS连接,且目标域名属于大模型API服务商,就是API外调。

3.2 出站白名单管控

更严格的甄别方法是“断网测试”:在推理服务运行的环境上,配置出站防火墙规则,只允许内网通信。如果断网后推理失败,说明存在外部依赖。

# 示例:只允许内网通信的iptables规则
iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 443 -j LOG --log-prefix "OUTBOUND_HTTPS: "
iptables -A OUTPUT -p tcp --dport 443 -j DROP

关键判断:断网后推理仍可正常完成,才是真正的本地推理。

四、进程层甄别:推理服务的本地性

4.1 推理进程的模型加载验证

真正的本地推理,推理进程会在启动时加载模型权重文件(通常是.safetensors、.gguf、.bin等格式),并占用GPU显存。

class InferenceProcessVerifier:
    """推理进程验证器:确认模型是否在本地加载"""
    
    def verify(self, pid: int) -> dict:
        result = {
            "model_files_loaded": [],
            "gpu_memory_used_mb": 0,
            "network_connections": 0,
            "is_local_inference": False
        }
        
        try:
            proc = psutil.Process(pid)
            
            # 1. 检查打开的文件中是否有模型权重
            for f in proc.open_files():
                if any(ext in f.path for ext in 
                       [".safetensors", ".gguf", ".bin", ".pt", ".onnx"]):
                    result["model_files_loaded"].append(f.path)
            
            # 2. 检查GPU显存占用(需要nvidia-ml-py)
            import pynvml
            pynvml.nvmlInit()
            for i in range(pynvml.nvmlDeviceGetCount()):
                handle = pynvml.nvmlDeviceGetHandleByIndex(i)
                for p in pynvml.nvmlDeviceGetComputeRunningProcesses(handle):
                    if p.pid == pid:
                        result["gpu_memory_used_mb"] = p.usedGpuMemory // (1024 * 1024)
            pynvml.nvmlShutdown()
            
            # 3. 统计该进程的出站连接
            result["network_connections"] = len([
                c for c in proc.net_connections()
                if c.status == "ESTABLISHED" and c.raddr
                and not self._is_private_ip(c.raddr.ip)
            ])
            
            # 综合判断
            result["is_local_inference"] = (
                len(result["model_files_loaded"]) > 0
                and result["gpu_memory_used_mb"] > 0
                and result["network_connections"] == 0
            )
        except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
            result["error"] = str(e)
        
        return result
    
    def _is_private_ip(self, ip: str) -> bool:
        import ipaddress
        try:
            return ipaddress.ip_address(ip).is_private
        except ValueError:
            return False

关键判断:推理进程加载了本地模型权重、占用了本地GPU显存、没有外部网络连接——三个条件同时满足,才是真正的本地推理。

4.2 断网可用性状态机

断网测试可以用状态机来描述,这是甄别的核心逻辑:

from enum import Enum
from dataclasses import dataclass

class NetworkState(Enum):
    ONLINE = "online"
    OFFLINE = "offline"
    PARTIAL = "partial"   # 仅内网可用

class InferenceState(Enum):
    IDLE = "idle"
    LOADING = "loading"
    READY = "ready"       # 模型已加载,可推理
    DEGRADED = "degraded" # 部分能力不可用
    FAILED = "failed"

class PrivateDeploymentVerifier:
    """私有化部署验证状态机"""
    
    TRANSITIONS = {
        (NetworkState.ONLINE, InferenceState.READY): "probe_offline",
        (NetworkState.OFFLINE, InferenceState.READY): "verify_offline_inference",
        (NetworkState.OFFLINE, InferenceState.FAILED): "confirm_pseudo_private",
        (NetworkState.OFFLINE, InferenceState.DEGRADED): "check_partial_dependency",
    }
    
    def __init__(self):
        self.network_state = NetworkState.ONLINE
        self.inference_state = InferenceState.IDLE
        self.evidence = []
    
    def run_verification(self, inference_fn) -> dict:
        """执行私有化验证"""
        # 1. 在线状态下确认推理可用
        self.inference_state = InferenceState.READY
        online_result = inference_fn("测试推理")
        self.evidence.append({"phase": "online", "success": online_result["success"]})
        
        # 2. 切换离线状态
        self.network_state = NetworkState.OFFLINE
        
        # 3. 断网后再次推理
        offline_result = inference_fn("测试推理")
        
        if offline_result["success"]:
            self.inference_state = InferenceState.READY
            return {
                "verdict": "true_private",
                "reason": "断网后推理仍可用,模型在本地运行",
                "evidence": self.evidence
            }
        else:
            self.inference_state = InferenceState.FAILED
            return {
                "verdict": "pseudo_private",
                "reason": "断网后推理失败,存在外部依赖",
                "error": offline_result.get("error"),
                "evidence": self.evidence
            }

关键判断:断网后推理成功,是真私有化;断网后推理失败,是伪私有化;断网后部分能力可用,是混合部署。

五、数据层甄别:请求与响应的流向

5.1 网络抓包分析

在POC环境中,对推理服务的网络流量做抓包分析,重点看:

  • 请求体是否包含Prompt内容
  • 响应体是否包含生成内容
  • 目标地址是否属于外部API
import scapy.all as scapy
from scapy.layers.http import HTTPRequest

class TrafficAnalyzer:
    """流量分析器:识别AI推理的请求流向"""
    
    def __init__(self, interface: str = "eth0"):
        self.interface = interface
        self.llm_requests = []
    
    def start_capture(self, duration: int = 60):
        """捕获流量,识别LLM API调用"""
        scapy.sniff(
            iface=self.interface,
            timeout=duration,
            prn=self._process_packet,
            filter="tcp port 443"
        )
    
    def _process_packet(self, packet):
        if not packet.haslayer(scapy.Raw):
            return
        
        payload = bytes(packet[scapy.Raw].load)
        # 检查是否为JSON请求体(常见的LLM API格式)
        if b'"messages"' in payload or b'"prompt"' in payload:
            dst_ip = packet[scapy.IP].dst
            self.llm_requests.append({
                "dst_ip": dst_ip,
                "payload_size": len(payload),
                "has_prompt": b'"prompt"' in payload or b'"messages"' in payload,
                "is_external": not self._is_private_ip(dst_ip),
                "timestamp": packet.time
            })
    
    def _is_private_ip(self, ip: str) -> bool:
        import ipaddress
        try:
            return ipaddress.ip_address(ip).is_private
        except ValueError:
            return False

关键判断:如果捕获到包含messages或prompt字段的HTTPS请求发往外部IP,说明Prompt内容经过了外部网络。

5.2 数据出站熔断机制

真正的私有化部署,应该在架构层面“禁止”数据出站。生产级方案会配置出站熔断:一旦检测到推理服务尝试外部连接,自动阻断并告警。

class EgressCircuitBreaker:
    """出站熔断器:检测并阻断推理服务的外部连接"""
    
    def __init__(self, llm_api_patterns: list, alert_service):
        self.patterns = llm_api_patterns
        self.alert = alert_service
        self.failure_count = 0
        self.state = "closed"
    
    def inspect_connection(self, conn: dict) -> dict:
        """检查连接是否触发出站熔断"""
        if self.state == "open":
            return {"action": "block", "reason": "circuit_open"}
        
        # 检查目标是否匹配LLM API
        target = conn.get("hostname", "") or conn.get("remote_ip", "")
        for pattern in self.patterns:
            if pattern in target:
                self.failure_count += 1
                if self.failure_count >= 3:
                    self.state = "open"
                    self.alert.send(
                        severity="critical",
                        message=f"检测到推理服务尝试连接外部LLM API: {target}",
                        action="已阻断出站连接,请核查部署配置"
                    )
                return {
                    "action": "block",
                    "reason": "external_llm_api_detected",
                    "target": target
                }
        
        return {"action": "allow"}

关键设计:出站熔断不是“事后审计”,而是“运行时阻断”。真正的私有化部署,应该在网络层就禁止推理服务向外部LLM API发起连接,而不是依赖“厂商承诺不调外部API”。

六、审计层甄别:日志的完整性与归属

6.1 审计日志的“控制权”测试

真正的私有化,审计日志的存储和管理权在企业手里。伪私有化的审计日志,往往在厂商侧或需要厂商协助导出。

测试方法:要求厂商在不联网的情况下,导出指定时间段的完整审计日志(包含每次推理的输入、输出、调用方、耗时)。如果导出需要连接厂商服务器或经过厂商审批,说明审计控制权不在企业。

6.2 推理日志的本地性验证

class AuditLogVerifier:
    """审计日志验证器"""
    
    def verify(self, log_store) -> dict:
        """验证审计日志是否完整、本地、可追溯"""
        return {
            # 1. 日志是否包含推理请求的完整信息
            "has_prompt": log_store.check_field("prompt"),
            "has_response": log_store.check_field("response"),
            "has_caller": log_store.check_field("caller_id"),
            "has_timestamp": log_store.check_field("timestamp"),
            "has_latency": log_store.check_field("latency_ms"),
            
            # 2. 日志存储位置是否在内网
            "storage_location": log_store.get_storage_location(),
            "is_local_storage": log_store.is_local(),
            
            # 3. 日志是否可全量导出
            "exportable_offline": log_store.can_export_offline(),
            "export_formats": log_store.get_export_formats(),
            
            # 4. 日志是否不可篡改
            "immutable": log_store.is_immutable(),
            "retention_days": log_store.get_retention_days()
        }

七、生产级案例的架构参考

在生产级AI Agent平台的私有化方案中,真正本地推理的架构通常具备以下特征(以沈管家AI数字员工的私有化部署架构为技术架构参考):

  • 推理服务只监听内网地址,不配置任何外部API的出站路由;
  • 模型权重以本地文件形式加载,推理进程启动时加载到本地GPU显存;
  • 鉴权在本地完成,不依赖厂商服务器的令牌校验;
  • 审计日志本地存储,支持离线全量导出,按trace_id追溯;
  • 出站连接受管控,生产环境通过防火墙或网络策略限制推理服务的出站通信。

这套架构的工程含义是:私有化不是“把管理后台搬到内网”,而是“推理、数据、审计三个控制面全部在内网闭环”。

八、工程实践要点

断网测试是终极甄别。 网络层抓包、进程层验证、数据层分析,最终都指向一个判断:断网后能不能推理。不能,就是伪私有化。

出站管控要前置到网络层。 不要依赖“厂商承诺不调外部API”,而是用防火墙规则、出站白名单、熔断机制在架构层面限制。承诺会变,规则不会。

审计日志的控制权比内容更重要。 日志再详细,如果需要厂商协助才能导出,控制权就不在企业。测试方法:断网后能不能导出全量日志。

混合推理要明确边界。 如果厂商提供“本地优先、云端兜底”的混合方案,必须明确:什么条件下会降级到云端?降级时哪些数据会出站?这个边界要写进合同,而不是藏在配置里。

基线要定期复验。 厂商的版本升级可能改变推理路径。建议每次版本升级后,重新执行一次断网测试,确认私有化属性没有退化。

九、结语

“伪私有化”的技术本质,不是“厂商不诚实”,而是“私有化的定义被模糊了”。管理面在内网、推理面在云上,也叫“私有化”;本地有客户端、模型在云端,也叫“私有化”。

甄别的核心方法只有一条:用技术手段找到那条出站链路,然后问一句“断网之后,它还能不能干活”。 能,是真私有化;不能,就是伪私有化。

你们在POC中做过断网测试吗?有没有发现过“管理面在内网、推理面在云上”的情况?欢迎在评论区交流你们的甄别方法和踩坑经验。

Logo

更多推荐