Skip to main content
在使用 LangSmith 跟踪时,您可能需要防止敏感信息被记录,以维护隐私并遵守安全要求。LangSmith 提供了多种方法来保护您的数据,防止其被发送到后端:
如果您的合规性或隐私要求规定某些操作根本不应被跟踪(例如,具有零保留策略的客户端),请考虑使用条件跟踪来选择性地禁用特定请求的跟踪,而不是对数据进行掩码处理。

隐藏输入和输出

如果您想完全隐藏跟踪的输入和输出,可以在运行应用程序时设置以下环境变量:
LANGSMITH_HIDE_INPUTS=true
LANGSMITH_HIDE_OUTPUTS=true
这适用于 LangSmith SDK(Python 和 TypeScript)以及 LangChain。 您还可以为给定的 Client 实例自定义和覆盖此行为。这可以通过在 Client 对象上设置 hide_inputshide_outputs 参数来完成(在 TypeScript 中为 hideInputshideOutputs)。 以下示例为 hide_inputshide_outputs 返回一个空对象,但您可以根据需要进行自定义:
import openai
from langsmith import Client
from langsmith.wrappers import wrap_openai

openai_client = wrap_openai(openai.Client())
langsmith_client = Client(
    hide_inputs=lambda inputs: {}, hide_outputs=lambda outputs: {}
)

# 生成的跟踪将包含其元数据,但输入将被隐藏
openai_client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Hello!"},
    ],
    langsmith_extra={"client": langsmith_client},
)

# 生成的跟踪将不会隐藏输入和输出
openai_client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Hello!"},
    ],
)

隐藏元数据

hide_metadata 参数允许您在使用 LangSmith Python SDK 进行跟踪时控制运行元数据是否被隐藏或转换。元数据在创建运行时通过 extra 参数传递(例如,extra={"metadata": {...}})。hide_metadata 对于移除敏感信息、遵守隐私要求或减少发送到 LangSmith 的数据量非常有用。您可以通过两种方式配置元数据隐藏:
  • 使用 SDK:
    from langsmith import Client
    
    client = Client(hide_metadata=True)
    
  • 使用环境变量:
    export LANGSMITH_HIDE_METADATA=true
    
hide_metadata 参数接受三种类型的值:
  • True:完全移除所有元数据(发送一个空字典)。
  • FalseNone:按原样保留元数据(默认行为)。
  • Callable:一个转换元数据字典的自定义函数。
设置后,此参数会影响由 Client 创建或更新的所有运行的 extra 参数中的 metadata 字段,包括通过 @traceable 装饰器或 LangChain 集成创建的运行。

隐藏所有元数据

设置 hide_metadata=True 以完全移除发送到 LangSmith 的运行中的所有元数据:
from langsmith import Client

# 完全隐藏所有元数据
client = Client(hide_metadata=True)

# 现在当您创建运行时,元数据将为空
client.create_run(
    "my_run",
    inputs={"question": "What is 2+2?"},
    run_type="llm",
    extra={"metadata": {"user_id": "123", "session": "abc"}}
)
# 发送到 LangSmith 的元数据将是 {} 而不是提供的元数据

自定义转换

使用可调用函数在元数据发送到 LangSmith 之前选择性地过滤、编辑或修改元数据:
# 移除敏感键
def hide_sensitive_metadata(metadata: dict) -> dict:
    return {k: v for k, v in metadata.items() if not k.startswith("_private")}

client = Client(hide_metadata=hide_sensitive_metadata)

# 编辑特定值
def redact_emails(metadata: dict) -> dict:
    import re
    result = {}
    for k, v in metadata.items():
        if isinstance(v, str) and "@" in v:
            result[k] = "[REDACTED_EMAIL]"
        else:
            result[k] = v
    return result

client = Client(hide_metadata=redact_emails)

# 添加转换标记
def add_marker(metadata: dict) -> dict:
    return {**metadata, "transformed": True}

client = Client(hide_metadata=add_marker)

基于规则的输入和输出掩码

此功能在以下 LangSmith SDK 版本中可用:
  • Python:0.1.81 及以上
  • TypeScript:0.1.33 及以上
要掩码输入和输出中的特定数据,您可以使用 create_anonymizer / createAnonymizer 函数,并在实例化 Client 时传递新创建的匿名化器。匿名化器可以由正则表达式模式列表和替换值构建,也可以由一个接受并返回字符串值的函数构建。 如果 LANGSMITH_HIDE_INPUTS = true,匿名化器将跳过输入。对于输出,如果 LANGSMITH_HIDE_OUTPUTS = true,同样适用。 但是,如果输入或输出要发送到 Clientanonymizer 方法将优先于在 hide_inputshide_outputs 中找到的函数。默认情况下,create_anonymizer 最多只会查看 10 层嵌套深度,这可以通过 max_depth 参数进行配置。
from langsmith.anonymizer import create_anonymizer
from langsmith import Client, traceable
import re

# 从正则表达式模式列表和替换值创建匿名化器
anonymizer = create_anonymizer([
    { "pattern": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}", "replace": "<email-address>" },
    { "pattern": r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", "replace": "<UUID>" }
])

# 或者从函数创建匿名化器
email_pattern = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}")
uuid_pattern = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
anonymizer = create_anonymizer(
    lambda text: email_pattern.sub("<email-address>", uuid_pattern.sub("<UUID>", text))
)

client = Client(anonymizer=anonymizer)

@traceable(client=client)
def main(inputs: dict) -> dict:
    ...
请注意,使用匿名化器可能会因复杂的正则表达式或大型负载而产生性能影响,因为匿名化器在处理前会将负载序列化为 JSON。
改进 anonymizer API 的性能已在我们的路线图上!如果您遇到性能问题,请通过 support.langchain.com 联系支持。
隐藏输入输出 旧版本的 LangSmith SDK 可以使用 hide_inputshide_outputs 参数来实现相同的效果。您也可以使用这些参数更高效地处理输入和输出。
import re
from langsmith import Client, traceable

# 定义电子邮件地址和 UUID 的正则表达式模式
EMAIL_REGEX = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}"
UUID_REGEX = r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"

def replace_sensitive_data(data, depth=10):
    if depth == 0:
        return data
    if isinstance(data, dict):
        return {k: replace_sensitive_data(v, depth-1) for k, v in data.items()}
    elif isinstance(data, list):
        return [replace_sensitive_data(item, depth-1) for item in data]
    elif isinstance(data, str):
        data = re.sub(EMAIL_REGEX, "<email-address>", data)
        data = re.sub(UUID_REGEX, "<UUID>", data)
        return data
    else:
        return data

client = Client(
    hide_inputs=lambda inputs: replace_sensitive_data(inputs),
    hide_outputs=lambda outputs: replace_sensitive_data(outputs)
)

inputs = {"role": "user", "content": "Hello! My email is user@example.com and my ID is 123e4567-e89b-12d3-a456-426614174000."}
outputs = {"role": "assistant", "content": "Hi! I've noted your email as user@example.com and your ID as 123e4567-e89b-12d3-a456-426614174000."}

@traceable(client=client)
def child(inputs: dict) -> dict:
    return outputs

@traceable(client=client)
def parent(inputs: dict) -> dict:
    child_outputs = child(inputs)
    return child_outputs

parent(inputs)

为单个函数处理输入和输出

process_outputs 参数在 LangSmith Python SDK 版本 0.1.98 及以上中可用。
除了 Client 级别的输入和输出处理外,LangSmith 还通过 @traceable 装饰器的 process_inputsprocess_outputs 参数提供函数级别的处理。 这些参数接受函数,允许您在特定函数的输入和输出记录到 LangSmith 之前对其进行转换。这对于减小负载大小、移除敏感信息或自定义对象在 LangSmith 中的序列化和表示方式非常有用。 以下是使用 process_inputsprocess_outputs 的示例:
from langsmith import traceable

def process_inputs(inputs: dict) -> dict:
    # inputs 是一个字典,其中键是参数名称,值是提供的参数
    # 返回一个包含处理后输入的新字典
    return {
        "processed_key": inputs.get("my_cool_key", "default"),
        "length": len(inputs.get("my_cool_key", ""))
    }

def process_outputs(output: Any) -> dict:
    # output 是函数的直接返回值
    # 将输出转换为字典
    # 在这种情况下,"output" 将是一个整数
    return {"processed_output": str(output)}

@traceable(process_inputs=process_inputs, process_outputs=process_outputs)
def my_function(my_cool_key: str) -> int:
    # 函数实现
    return len(my_cool_key)

result = my_function("example")
在此示例中,process_inputs 创建一个包含处理后输入数据的新字典,process_outputs 在记录到 LangSmith 之前将输出转换为特定格式。
建议避免在处理器函数中修改源对象。而是创建并返回包含处理数据的新对象。
对于异步函数,用法类似:
@traceable(process_inputs=process_inputs, process_outputs=process_outputs)
async def async_function(key: str) -> int:
    # 异步实现
    return len(key)
当同时定义了函数级别处理器和 Client 级别处理器(hide_inputshide_outputs)时,函数级别处理器优先。

示例

您可以将基于规则的掩码与各种匿名化器结合使用,以从输入和输出中清除敏感信息。以下示例将涵盖使用正则表达式、Microsoft Presidio 和 Amazon Comprehend。

正则表达式

下面的实现并非详尽无遗,可能会遗漏某些格式或边缘情况。在生产环境中使用任何实现之前,请进行彻底测试。
您可以使用正则表达式在输入和输出发送到 LangSmith 之前对其进行掩码。下面的实现掩码了电子邮件地址、电话号码、全名、信用卡号和社会安全号码。
import re
import openai
from langsmith import Client
from langsmith.wrappers import wrap_openai

# 定义各种个人身份信息的正则表达式模式
SSN_PATTERN = re.compile(r'\b\d{3}-\d{2}-\d{4}\b')
CREDIT_CARD_PATTERN = re.compile(r'\b(?:\d[ -]*?){13,16}\b')
EMAIL_PATTERN = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b')
PHONE_PATTERN = re.compile(r'\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b')
FULL_NAME_PATTERN = re.compile(r'\b([A-Z][a-z]*\s[A-Z][a-z]*)\b')

def regex_anonymize(text):
    """
    使用正则表达式模式匿名化文本中的敏感信息。
    参数:
        text (str): 要匿名化的输入文本。
    返回:
        str: 匿名化后的文本。
    """
    # 用占位符替换敏感信息
    text = SSN_PATTERN.sub('[REDACTED SSN]', text)
    text = CREDIT_CARD_PATTERN.sub('[REDACTED CREDIT CARD]', text)
    text = EMAIL_PATTERN.sub('[REDACTED EMAIL]', text)
    text = PHONE_PATTERN.sub('[REDACTED PHONE]', text)
    text = FULL_NAME_PATTERN.sub('[REDACTED NAME]', text)
    return text

def recursive_anonymize(data, depth=10):
    """
    递归遍历数据结构并匿名化敏感信息。
    参数:
        data (any): 要匿名化的输入数据。
        depth (int): 当前递归深度,以防止过度递归。
    返回:
        any: 匿名化后的数据。
    """
    if depth == 0:
        return data
    if isinstance(data, dict):
        anonymized_dict = {}
        for k, v in data.items():
            anonymized_value = recursive_anonymize(v, depth - 1)
            anonymized_dict[k] = anonymized_value
        return anonymized_dict
    elif isinstance(data, list):
        anonymized_list = []
        for item in data:
            anonymized_item = recursive_anonymize(item, depth - 1)
            anonymized_list.append(anonymized_item)
        return anonymized_list
    elif isinstance(data, str):
        anonymized_data = regex_anonymize(data)
        return anonymized_data
    else:
        return data

openai_client = wrap_openai(openai.Client())

# 使用匿名化函数初始化 LangSmith @[Client]
langsmith_client = Client(
    hide_inputs=recursive_anonymize, hide_outputs=recursive_anonymize
)

# 生成的跟踪将包含其元数据,但输入和输出将被匿名化
response_with_anonymization = openai_client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "My name is John Doe, my SSN is 123-45-6789, my credit card number is 4111 1111 1111 1111, my email is john.doe@example.com, and my phone number is (123) 456-7890."},
    ],
    langsmith_extra={"client": langsmith_client},
)

# 生成的跟踪将不会匿名化输入和输出
response_without_anonymization = openai_client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "My name is John Doe, my SSN is 123-45-6789, my credit card number is 4111 1111 1111 1111, my email is john.doe@example.com, and my phone number is (123) 456-7890."},
    ],
)
匿名化后的运行在 LangSmith 中将如下所示:匿名化运行 未匿名化的运行在 LangSmith 中将如下所示:未匿名化运行

Microsoft Presidio

下面的实现提供了一个通用示例,说明如何匿名化用户与 LLM 之间交换的消息中的敏感信息。它并非详尽无遗,也未涵盖所有情况。在生产环境中使用任何实现之前,请进行彻底测试。
Microsoft Presidio 是一个数据保护和去标识化 SDK。下面的实现使用 Presidio 在输入和输出发送到 LangSmith 之前对其进行匿名化。有关最新信息,请参阅 Presidio 的官方文档 要使用 Presidio 及其 spaCy 模型,请安装以下内容:
pip install presidio-analyzer
pip install presidio-anonymizer
python -m spacy download en_core_web_lg
同时,安装 OpenAI:
pip install openai
import openai
from langsmith import Client
from langsmith.wrappers import wrap_openai
from presidio_anonymizer import AnonymizerEngine
from presidio_analyzer import AnalyzerEngine

anonymizer = AnonymizerEngine()
analyzer = AnalyzerEngine()

def presidio_anonymize(data):
    """
    匿名化用户发送或模型返回的敏感信息。
    参数:
        data (any): 要匿名化的数据。
    返回:
        any: 匿名化后的数据。
    """
    message_list = (
        data.get('messages') or [data.get('choices', [{}])[0].get('message')]
    )
    if not message_list or not all(isinstance(msg, dict) and msg for msg in message_list):
        return data

    for message in message_list:
        content = message.get('content', '')
        if not content.strip():
            print("检测到空内容。跳过匿名化。")
            continue

        results = analyzer.analyze(
            text=content,
            entities=["PERSON", "PHONE_NUMBER", "EMAIL_ADDRESS", "US_SSN"],
            language='en'
        )
        anonymized_result = anonymizer.anonymize(
            text=content,
            analyzer_results=results
        )
        message['content'] = anonymized_result.text

    return data

openai_client = wrap_openai(openai.Client())

# 使用匿名化函数初始化 langsmith @[Client]
langsmith_client = Client(
  hide_inputs=presidio_anonymize, hide_outputs=presidio_anonymize
)

# 生成的跟踪将包含其元数据,但输入和输出将被匿名化
response_with_anonymization = openai_client.chat.completions.create(
  model="gpt-5.4-mini",
  messages=[
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at real.slim.shady@gmail.com"},
  ],
  langsmith_extra={"client": langsmith_client},
)

# 生成的跟踪将不会匿名化输入和输出
response_without_anonymization = openai_client.chat.completions.create(
  model="gpt-5.4-mini",
  messages=[
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at real.slim.shady@gmail.com"},
  ],
)
匿名化后的运行在 LangSmith 中将如下所示:匿名化运行 未匿名化的运行在 LangSmith 中将如下所示:未匿名化运行

Amazon Comprehend

下面的实现提供了一个通用示例,说明如何匿名化用户与 LLM 之间交换的消息中的敏感信息。它并非详尽无遗,也未涵盖所有情况。在生产环境中使用任何实现之前,请进行彻底测试。
Comprehend 是一项自然语言处理服务,可以检测个人身份信息。下面的实现使用 Comprehend 在输入和输出发送到 LangSmith 之前对其进行匿名化。有关最新信息,请参阅 Comprehend 的官方文档 要使用 Comprehend,请安装 boto3
pip install boto3
同时,安装 OpenAI:
pip install openai
您需要在 AWS 中设置凭据并使用 AWS CLI 进行身份验证。请按照 AWS Comprehend 设置说明操作。
import openai
import boto3
from langsmith import Client
from langsmith.wrappers import wrap_openai

comprehend = boto3.client('comprehend', region_name='us-east-1')

def redact_pii_entities(text, entities):
    """
    根据检测到的实体编辑文本中的个人身份信息实体。
    参数:
        text (str): 包含个人身份信息的原始文本。
        entities (list): 检测到的个人身份信息实体列表。
    返回:
        str: 编辑了个人身份信息实体后的文本。
    """
    sorted_entities = sorted(entities, key=lambda x: x['BeginOffset'], reverse=True)
    redacted_text = text
    for entity in sorted_entities:
        begin = entity['BeginOffset']
        end = entity['EndOffset']
        entity_type = entity['Type']
        # 根据实体类型定义编辑占位符
        placeholder = f"[{entity_type}]"
        # 用占位符替换文本中的个人身份信息
        redacted_text = redacted_text[:begin] + placeholder + redacted_text[end:]
    return redacted_text

def detect_pii(text):
    """
    使用 AWS Comprehend 检测给定文本中的个人身份信息实体。
    参数:
        text (str): 要分析的文本。
    返回:
        list: 检测到的个人身份信息实体列表。
    """
    try:
        response = comprehend.detect_pii_entities(
            Text=text,
            LanguageCode='en',
        )
        entities = response.get('Entities', [])
        return entities
    except Exception as e:
        print(f"检测个人身份信息时出错: {e}")
        return []

def comprehend_anonymize(data):
    """
    匿名化用户发送或模型返回的敏感信息。
    参数:
        data (any): 要匿名化的输入数据。
    返回:
        any: 匿名化后的数据。
    """
    message_list = (
        data.get('messages') or [data.get('choices', [{}])[0].get('message')]
    )
    if not message_list or not all(isinstance(msg, dict) and msg for msg in message_list):
        return data

    for message in message_list:
        content = message.get('content', '')
        if not content.strip():
            print("检测到空内容。跳过匿名化。")
            continue

        entities = detect_pii(content)
        if entities:
            anonymized_text = redact_pii_entities(content, entities)
            message['content'] = anonymized_text
        else:
            print("未检测到个人身份信息。内容保持不变。")

    return data

openai_client = wrap_openai(openai.Client())

# 使用匿名化函数初始化 langsmith @[Client]
langsmith_client = Client(
  hide_inputs=comprehend_anonymize, hide_outputs=comprehend_anonymize
)

# 生成的跟踪将包含其元数据,但输入和输出将被匿名化
response_with_anonymization = openai_client.chat.completions.create(
  model="gpt-5.4-mini",
  messages=[
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at real.slim.shady@gmail.com"},
  ],
  langsmith_extra={"client": langsmith_client},
)

# 生成的跟踪将不会匿名化输入和输出
response_without_anonymization = openai_client.chat.completions.create(
  model="gpt-5.4-mini",
  messages=[
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at real.slim.shady@gmail.com"},
  ],
)
匿名化后的运行在 LangSmith 中将如下所示:匿名化运行 未匿名化的运行在 LangSmith 中将如下所示:未匿名化运行

用于高吞吐量掩码的批量处理

本页上的先前方法是单独处理每个运行的。如果您的掩码逻辑涉及速率受限的 API 或模型推理(例如 Presidio 或 Amazon Comprehend 示例),逐个处理运行可能会造成瓶颈。process_buffered_run_ops 允许您在原始运行字典序列化并发送到 API 之前拦截一批原始运行字典,因此您可以一次性分摊多个运行的成本。LangSmith 在后台线程中处理这些运行,不会阻塞您的应用程序。 LangSmith 将运行保存在内存缓冲区中,并在以下情况下将它们作为一批刷新:
  • 已累积 run_ops_buffer_size 个运行操作,或
  • 自上次添加运行以来已过 run_ops_buffer_timeout_ms 毫秒(默认:5000 毫秒)。
您的函数接收一批原始运行字典,并且必须返回一个相同长度相同顺序运行 ID 未更改的列表。违反任一约束都会引发 ValueError
run_ops_buffer_size 计算的是单个运行操作,而不是唯一的运行。每个跟踪调用通常产生两个操作:一个创建操作(当运行开始时)和一个更新操作(当它以输出结束时)。相应地设置缓冲区大小。例如,run_ops_buffer_size=1000 将缓冲大约 500 个跟踪调用。因此,同一个运行 ID 可能在一个批次中出现两次:一次带有输入,一次带有输出。
缓冲区仅在达到大小限制或超时时自动刷新。始终在程序退出前调用 client.flush(),以避免丢失缓冲的运行。
批次中的每个运行字典要么是创建操作(带有 inputs,在运行开始时发送),要么是更新操作(带有 outputs,在运行结束时发送)。以下是单个跟踪调用的典型对示例:
# 创建操作 — 在运行开始时发送
{
    "id": "018f1b2c-...",
    "name": "my_llm_call",
    "run_type": "llm",
    "inputs": {"messages": [{"role": "user", "content": "My name is Jane Smith..."}]},
    "start_time": "2024-01-01T00:00:00.000Z",
    "trace_id": "018f1b2c-...",
    "dotted_order": "20240101T000000000000Z018f1b2c-...",
    "extra": {"metadata": {}, "runtime": {...}},
    "session_name": "default",
}

# 更新操作 — 在运行结束时发送(相同的 id,添加 outputs)
{
    "id": "018f1b2c-...",
    "outputs": {"choices": [{"message": {"role": "assistant", "content": "Hello Jane..."}}]},
    "end_time": "2024-01-01T00:00:01.000Z",
    "trace_id": "018f1b2c-...",
    "dotted_order": "20240101T000000000000Z018f1b2c-...",
}
以下示例使用 Comprehend 的 batch_detect_pii_entities 端点,该端点每次调用最多接受 25 个文本。使用按运行处理的方法(hide_inputs),您需要为每个运行进行一次 API 调用。在这里,首先收集整个缓冲区中的所有消息文本,然后以 25 个为一批发送到 Comprehend,这在高吞吐量下显著减少了 API 调用次数。
import boto3
from langsmith import Client, traceable

comprehend = boto3.client("comprehend", region_name="us-east-1")

def redact_entities(text: str, entities: list) -> str:
    for entity in sorted(entities, key=lambda e: e["BeginOffset"], reverse=True):
        placeholder = f"[{entity['Type']}]"
        text = text[:entity["BeginOffset"]] + placeholder + text[entity["EndOffset"]:]
    return text

def comprehend_anonymize_batch(runs: list[dict]) -> list[dict]:
    # 收集所有消息文本并记住它们来自哪里。
    # 注意:同一个运行 ID 可能出现两次 — 一次作为创建操作(带有 inputs)
    # 一次作为更新操作(带有 outputs)。
    locations = []  # (run_idx, field, msg_idx)
    texts = []
    for run_idx, run in enumerate(runs):
        for field in ("inputs", "outputs"):
            data = run.get(field)
            if not isinstance(data, dict):
                continue
            for msg_idx, message in enumerate(data.get("messages") or []):
                content = message.get("content", "")
                if content.strip():
                    locations.append((run_idx, field, msg_idx))
                    texts.append(content)

    # 将所有文本以 25 个为一批发送到 Comprehend(API 限制)。
    # 对于 1000 个操作(约 500 个运行),每个运行有 2 条消息:40 次 API 调用而不是 1000 次。
    redacted_texts = []
    for i in range(0, len(texts), 25):
        chunk = texts[i : i + 25]
        response = comprehend.batch_detect_pii_entities(
            TextList=chunk, LanguageCode="en"
        )
        for text, result in zip(chunk, response["ResultList"]):
            redacted_texts.append(redact_entities(text, result.get("Entities", [])))

    # 将编辑后的文本写回运行字典
    for (run_idx, field, msg_idx), redacted in zip(locations, redacted_texts):
        runs[run_idx][field]["messages"][msg_idx]["content"] = redacted

    return runs

client = Client(
    process_buffered_run_ops=comprehend_anonymize_batch,
    run_ops_buffer_size=1000,        # 约 500 个跟踪调用(每个调用 2 个操作:创建 + 更新)
    run_ops_buffer_timeout_ms=3000,  # 或 3 秒后,以先到者为准
)

@traceable(client=client)
def my_llm_call(messages: list) -> dict:
    # ... 您的 LLM 调用 ...
    pass

try:
    my_llm_call([{"role": "user", "content": "My name is Jane Smith, call me at 555-867-5309"}])
finally:
    client.flush()  # 始终在退出前刷新
process_buffered_run_opsrun_ops_buffer_size 必须始终一起设置——只提供其中一个而不提供另一个会引发 ValueError