【LangChain】Context与Runtime:运行时数据注入完全指南

0 阅读8分钟

"为什么我的工具总是拿不到用户ID?"

如果你也曾被这个问题折磨到凌晨三点,这篇文章就是为你准备的。

在构建生产级Agent时,数据传递往往是最让人头疼的环节。LangChain v1.0 引入的 Runtime 机制,本质上解决了一个核心问题:如何让工具"感知"到运行时的上下文,同时又不把这些内部细节暴露给大模型?

想象一下快递配送:用户(LLM)只需要填写收货地址,但快递员(Tool)还需要知道配送站点、客户等级、甚至实时交通状况。Runtime就是那个"快递员专用信息包"——对用户透明,对工具必需。


一、Runtime核心能力

LangChain 的 ToolRuntime 为工具提供了六大核心能力 :

组件作用域典型用途类比
State当前对话访问消息历史、计数器、临时标记工作台(当前任务相关)
Context单次运行用户ID、权限、数据库连接、API密钥工牌(身份认证信息)
Store跨会话持久化用户偏好、长期记忆、知识库档案柜(历史记录)
Stream Writer实时流进度反馈、中间状态推送对讲机(实时通讯)
Config运行配置回调函数、标签、元数据配置手册
Tool Call ID单次调用日志追踪、调用链关联工单号

代码速览:一个完整的 ToolRuntime 工具

from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime

@dataclass
class Context:
    user_id: str
    user_role: str  # "admin" | "user" | "guest"

@tool
def fetch_user_data(query: str, runtime: ToolRuntime[Context]) -> str:
    """
    根据查询获取用户数据,自动进行权限检查。
    
    Args:
        query: 用户查询内容
    """
    # 1. 从 Context 获取身份信息
    user_id = runtime.context.user_id
    role = runtime.context.user_role
    
    # 2. 从 State 读取对话历史(检查是否有敏感操作前置)
    messages = runtime.state.get("messages", [])
    
    # 3. 从 Store 获取用户长期偏好
    if runtime.store:
        prefs = runtime.store.get(("user_prefs",), user_id)
    
    # 4. 实时推送进度
    runtime.stream_writer(f"🔍 正在为用户 {user_id} 查询数据...")
    
    # 5. 权限检查(敏感信息隔离示例)
    if "salary" in query.lower() and role != "admin":
        return "❌ 权限不足:薪资信息仅限管理员查询"
    
    return f"查询结果 for {user_id}: ..."

二、Context Schema:类型安全的"数据契约"

context_schema 是 LangChain v1.0 最重要的设计改进之一

在旧版本中,传递额外数据需要通过 config["configurable"] 这种"黑魔法"方式。新版本的 context_schema 让这一切变得显式、类型安全、可维护

基础用法:Dataclass 定义

from dataclasses import dataclass
from langchain.agents import create_agent

@dataclass
class AppContext:
    user_id: str
    tenant_id: str  # 多租户隔离
    request_id: str  # 用于日志追踪
    feature_flags: dict  # 功能开关

agent = create_agent(
    model="gpt-4o",
    tools=[fetch_user_data, update_settings],
    context_schema=AppContext  # 声明上下文结构
)

# 调用时注入
result = agent.invoke(
    {"messages": [{"role": "user", "content": "查看我的数据"}]},
    context=AppContext(
        user_id="user_123",
        tenant_id="tenant_456",
        request_id="req_789",
        feature_flags={"beta_feature": True}
    )
)

进阶:Pydantic 与验证

对于更复杂的场景,Pydantic 提供了更强的验证能力:

from pydantic import BaseModel, Field, validator

class SecureContext(BaseModel):
    user_id: str = Field(..., min_length=5)
    permissions: list[str] = Field(default_factory=list)
    session_start: datetime = Field(default_factory=datetime.now)
    
    @validator('permissions')
    def validate_permissions(cls, v):
        allowed = {"read", "write", "delete", "admin"}
        invalid = set(v) - allowed
        if invalid:
            raise ValueError(f"无效权限: {invalid}")
        return v

# 在工具中使用
@tool
def delete_resource(resource_id: str, runtime: ToolRuntime[SecureContext]) -> str:
    """删除资源,需要 write 或 admin 权限"""
    if "admin" not in runtime.context.permissions:
        raise PermissionError("需要管理员权限")
    # 执行删除...

三、实战模式:用户身份与权限传递

生产环境的核心诉求:不同用户看到不同数据。

模式1:基于角色的数据过滤(RBAC)

from enum import Enum
from dataclasses import dataclass

class Role(Enum):
    ADMIN = "admin"
    MANAGER = "manager"
    USER = "user"

@dataclass
class AuthContext:
    user_id: str
    role: Role
    department: str | None = None

@tool
def query_sales_data(
    quarter: str, 
    runtime: ToolRuntime[AuthContext]
) -> str:
    """
    查询销售数据,自动根据角色过滤可见范围。
    """
    ctx = runtime.context
    
    # 权限矩阵
    if ctx.role == Role.ADMIN:
        # 管理员:查看全公司数据
        data = fetch_all_sales(quarter)
    elif ctx.role == Role.MANAGER:
        # 经理:仅限本部门
        data = fetch_department_sales(quarter, ctx.department)
    else:
        # 普通用户:仅个人数据
        data = fetch_personal_sales(quarter, ctx.user_id)
    
    # 审计日志(写入Store)
    if runtime.store:
        runtime.store.put(
            ("audit",), 
            f"{ctx.user_id}_{datetime.now().isoformat()}",
            {"action": "query_sales", "quarter": quarter, "role": ctx.role.value}
        )
    
    return format_sales_report(data)

模式2:多租户隔离

@dataclass
class TenantContext:
    tenant_id: str
    user_id: str
    db_pool: AsyncConnectionPool  # 数据库连接池 

@tool
async def get_customer_list(
    filter_status: str | None = None,
    runtime: ToolRuntime[TenantContext]
) -> str:
    """
    获取客户列表,自动隔离租户数据。
    """
    tenant_id = runtime.context.tenant_id
    pool = runtime.context.db_pool
    
    # SQL 自动注入 tenant_id 过滤
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            "SELECT * FROM customers WHERE tenant_id = $1 AND ($2::text IS NULL OR status = $2)",
            tenant_id, filter_status
        )
    
    return json.dumps([dict(r) for r in rows])

⚠️ 重要提示:当使用 LangGraph Server 部署时,无法直接通过 .invoke() 注入 context。此时需要在 graph 启动时初始化资源,或通过 configurable 传递配置 。


四、敏感信息隔离:安全最佳实践

"不要把数据库密码传给 LLM" —— 这听起来理所当然,但在复杂的工具链中很容易出错。

安全原则 checklist

Context 中的敏感信息:API密钥、数据库连接、加密密钥
State 中的会话信息:认证状态、临时token、上传文件
Store 中的持久化数据:用户密码哈希、隐私设置
绝不暴露给 LLM:通过 context_schema 定义的数据不会出现在工具的 JSON Schema 中

实战:安全的支付工具

from dataclasses import dataclass
from typing import Annotated
from langchain.tools import InjectedToolCallId

@dataclass
class PaymentContext:
    user_id: str
    # 这些字段对 LLM 不可见,但工具可以访问
    stripe_api_key: str  
    fraud_check_endpoint: str
    max_transaction_amount: Decimal

@tool
def process_payment(
    amount: Decimal,
    currency: str,
    runtime: ToolRuntime[PaymentContext],
    # 这个参数会被自动注入,不会暴露给 LLM
    tool_call_id: Annotated[str, InjectedToolCallId]
) -> str:
    """
    处理支付请求,包含风控检查。
    
    Args:
        amount: 支付金额(如 99.99)
        currency: 货币代码(如 "USD")
    """
    ctx = runtime.context
    
    # 1. 风控检查(使用内部 API,LLM 不知道 endpoint)
    risk_score = check_fraud(
        ctx.fraud_check_endpoint,
        ctx.user_id, 
        amount,
        currency
    )
    
    if risk_score > 0.8:
        # 记录到长期记忆
        if runtime.store:
            runtime.store.put(
                ("security",), 
                ctx.user_id,
                {"last_blocked": datetime.now().isoformat(), "reason": "high_risk"}
            )
        return "⚠️ 交易被风控系统拦截,请联系客服"
    
    # 2. 限额检查
    if amount > ctx.max_transaction_amount:
        return f"❌ 超出单笔限额 {ctx.max_transaction_amount}"
    
    # 3. 实时通知用户进度
    runtime.stream_writer("🔒 正在连接支付网关...")
    
    # 4. 执行支付(使用密钥,LLM 无法获取)
    result = stripe_charge(
        api_key=ctx.stripe_api_key,  # 安全注入
        amount=amount,
        currency=currency,
        metadata={"user_id": ctx.user_id, "tool_call_id": tool_call_id}
    )
    
    runtime.stream_writer("✅ 支付处理完成")
    return f"交易成功,ID: {result.id}"


五、Stream Writer:实时反馈的艺术

长耗时工具(如数据分析、文件处理)最大的用户体验杀手是**"假死"状态**。

runtime.stream_writer 让你可以在工具执行过程中推送实时更新

@tool
def analyze_large_dataset(
    dataset_id: str,
    analysis_type: str,
    runtime: ToolRuntime
) -> str:
    """
    分析大型数据集,实时报告进度。
    """
    writer = runtime.stream_writer
    
    writer({"type": "status", "message": "📥 正在加载数据集..."})
    df = load_dataset(dataset_id)
    
    writer({"type": "progress", "percent": 20, "message": "🔍 数据清洗中..."})
    df_clean = clean_data(df)
    
    writer({"type": "progress", "percent": 50, "message": "🧮 执行统计分析..."})
    stats = compute_statistics(df_clean)
    
    writer({"type": "progress", "percent": 80, "message": "📊 生成可视化..."})
    charts = generate_charts(stats)
    
    writer({"type": "complete", "message": "✅ 分析完成!"})
    
    return format_report(stats, charts)

前端配合(React 示例):

// 使用 LangChain 的 useStream hook
const { stream } = useStream();

stream.subscribe((chunk) => {
  if (chunk.type === 'progress') {
    updateProgressBar(chunk.percent);
    showStatus(chunk.message);
  }
});

六、完整实战:企业级 Agent 架构

把以上概念整合到一个真实的客服 Agent 场景:

from dataclasses import dataclass
from datetime import datetime
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, AgentState
from langchain.tools import tool, ToolRuntime

# ============ 1. 上下文定义 ============
@dataclass
class CustomerServiceContext:
    agent_id: str           # 客服工号
    user_tier: str          # "vip" | "premium" | "standard"
    session_id: str         # 用于全链路追踪
    crm_api_token: str      # 敏感:CRM系统密钥
    knowledge_base_version: str

# ============ 2. 中间件:动态权限提示 ============
@before_model
def inject_privacy_warning(state: AgentState, runtime: Runtime[CustomerServiceContext]) -> dict:
    """在每次模型调用前注入数据隐私提醒"""
    if runtime.context.user_tier == "vip":
        return {
            "messages": [
                {"role": "system", "content": "⚠️ 当前用户为VIP,注意保护隐私数据,不得透露其他客户信息"}
            ]
        }
    return None

# ============ 3. 工具实现 ============
@tool
def lookup_customer_history(
    customer_phone: str,
    runtime: ToolRuntime[CustomerServiceContext]
) -> str:
    """查询客户历史记录,自动根据 tier 决定详细程度"""
    ctx = runtime.context
    
    # 实时反馈
    runtime.stream_writer(f"🔍 正在查询客户 {customer_phone[-4:]}...")
    
    # 使用安全 token 调用 CRM
    history = crm_client.query(
        token=ctx.crm_api_token,
        phone=customer_phone
    )
    
    # VIP 客户看到完整历史,普通客户仅看摘要
    if ctx.user_tier == "vip":
        return format_detailed_history(history)
    else:
        return format_summary(history)

@tool
def escalate_to_human(
    reason: str,
    runtime: ToolRuntime[CustomerServiceContext]
) -> str:
    """升级至人工客服"""
    ctx = runtime.context
    
    # 写入长期记忆:标记该用户需要人工跟进
    if runtime.store:
        runtime.store.put(
            ("escalations",),
            ctx.session_id,
            {
                "agent_id": ctx.agent_id,
                "reason": reason,
                "timestamp": datetime.now().isoformat(),
                "resolved": False
            }
        )
    
    # 通知监控系统
    runtime.stream_writer({
        "type": "alert",
        "level": "high",
        "message": f"工单升级: {reason}"
    })
    
    return "已为您转接人工客服,请稍候..."

# ============ 4. 组装 Agent ============
agent = create_agent(
    model="claude-3-5-sonnet",
    tools=[lookup_customer_history, escalate_to_human, process_refund],
    context_schema=CustomerServiceContext,
    middleware=[inject_privacy_warning],
    store=InMemoryStore()  # 生产环境使用 RedisStore
)

# ============ 5. 运行 ============
response = agent.invoke(
    {"messages": [{"role": "user", "content": "我要投诉昨天的订单"}]},
    context=CustomerServiceContext(
        agent_id="CS-10086",
        user_tier="vip",
        session_id="sess-2025-001",
        crm_api_token=os.getenv("CRM_TOKEN"),  # 从环境变量安全获取
        knowledge_base_version="v2.3"
    )
)


总结:设计哲学

LangChain v1.0 的 Runtime 机制体现了一个核心设计哲学:

"显式优于隐式,类型安全优于灵活,安全优先于便利。"

通过 context_schema,我们获得了:

  • 类型安全:IDE 自动补全和静态检查
  • 安全隔离:敏感信息对 LLM 完全不可见
  • 可测试性:可以 Mock Context 进行单元测试
  • 可维护性:数据流向清晰,不依赖"魔法"配置

关注公众号【dev派】,发送 "agent" 获取全部源码和模板