跳转至

Agent 调试工具

LangSmith 的 Trace 分析与 Agent 调试最佳实践

🔎 LangSmith 的 Trace 是什么?

▎先扔结论

Trace 不是简单的日志,它是 LLM 应用的一次完整“调用树”记录,包含每一步的输入、输出、耗时、token 消耗、元数据以及它们之间的嵌套关系。

换句话说,如果你把一次 Agent 调用看作一棵树,那么 Trace 就是这棵树在 LangSmith 里的数字孪生。根节点是你发起的请求,子节点可能是 LLM 调用、工具执行、检索器查询、chain 步骤等,一层层展开,直到每一个原子操作。

▎Trace 里究竟记了什么?

一个典型的 Trace 是由多个 Run 组成的树形结构。每个 Run 都会精确记录:

  • 输入/输出:完整的 prompt 文本、模型的原始返回、工具调用的参数和结果。

  • 时序数据:开始时间、结束时间、首 token 时间,延迟一目了然。

  • Token 用量:prompt tokens、completion tokens、总 tokens,成本立即可算。

  • 元数据与标签:你可以自己打上 user_idsession_idfeature_flag 等,方便后续筛选。

  • 反馈:用户点踩、人工评分、自动评估器打分,直接挂在 Run 上。

▎为什么要这么设计?

传统日志是扁平的,你只能看到“调了什么函数,返回了什么”。但当一次请求里包含 3 次 LLM 调用、2 次工具调用、1 次检索时,扁平日志会让你抓狂。Trace 的树状结构天然对应了 Agent 的推理步骤,哪里卡顿、哪里传错了参数,一眼定位。

▎代码里怎么看到 Trace?

只要你设置了环境变量,LangChain 会自动把每次调用发到 LangSmith。

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY="ls__..."
export LANGCHAIN_PROJECT="my-agent"

然后在代码里正常调用,比如:

from langchain_openai import ChatOpenAI
from langchain.agents import create_openai_functions_agent, AgentExecutor

llm = ChatOpenAI(model="gpt-4o", temperature=0)
agent_executor = AgentExecutor(agent=agent, tools=tools)
agent_executor.invoke({"input": "帮我查一下北京天气并总结"})

这时打开 LangSmith UI,你就会看到一条包含 AgentExecutorLLMTool 等层层嵌套的 Trace。点击任何一个 Run,prompt 原文、工具返回的 JSON、模型思考过程一览无余。

🧠 面试这样答直接拉开差距 “Trace 本质是 LLM 应用的可观测性基石,它把一次请求的所有 LLM 和工具交互结构化成一棵树。有了它,我们才能做后续的调试、评估、成本分析和回归测试,而不是对着控制台打印的零散日志瞎猜。”


🩺 2. 如何用 LangSmith 定位 Agent 问题,并做 Prompt 版本优化和成本分析?

这道题面试官在考察你是不是真的用过 LangSmith 做工程优化,而不是只会点开 UI 看错误。我们分三块来讲。

▎📍 问题定位:像法医一样解剖 Agent 的每一步

Agent 常见的问题:调错工具、循环死锁、幻觉答案、输出格式异常。用 LangSmith 定位的常规路径如下:

  1. 过滤出问题会话 如果线上有用户反馈,直接根据 run_id 或自定义的 metadata.user_id 搜索 Trace。没有反馈也没关系,可以通过 LangSmith 的过滤器,找出所有包含 Error 状态或 feedback.score = 0 的 Run。

  2. 展开 Trace 树,逐节点排查 打开一个异常 Trace,从左边的树形结构开始:

  3. 根节点看总延迟和总 token 是否异常。
  4. 进入第一个 LLM Run,检查 输入 prompt 是否包含了正确的上下文。有时 RAG 检索回来的文档根本不相关,模型自然答非所问。
  5. 查看 输出,如果模型的输出本该是函数调用,却给了自然语言,说明 prompt 约束不够或模型本身不稳定。
  6. 再进入工具 Run,检查传入的参数是否符合预期,工具返回的错误信息是不是被模型误吞了。

  7. 利用“对比模式” LangSmith 允许你选中两次 Run 进行左右对比,比如一次成功、一次失败。你可以直观看到,到底是哪个步骤的输入产生了偏差,比如检索结果的排序不同,导致模型没读到关键段落。

实战技巧:我通常会在 Agent 的每个关键节点加一个 with langsmith.trace 上下文,手动注入一些调试信息,或者用 run.collect 把中间变量挂到 trace 上,这样排查效率更高。

▎📝 Prompt 版本优化:告别“凭感觉改词”

Prompt 优化如果不做版本管理,基本等于玄学。LangSmith 的 Hub 功能就是解决这个的。

工作流:

  1. 把 prompt 推送到 Hub,每次修改都会自动生成 commit,像 Git 一样。
from langchain import hub
hub.push("my-agent-prompt", prompt_template)
  1. 跑一批测试用例,关联到不同 prompt 版本。可以使用 LangSmith 的数据集功能,把用户历史问题沉淀成评估集。

  2. langsmith.evaluation 里的评估器,或者自己写一个,对每个 prompt 版本的输出做自动打分(如正确性、相关性、格式规范)。

  3. 在线下比较不同版本的平均分和个案表现,选出最优版本后再上线。你甚至可以做 A/B 测试:让 5% 的线上流量走新版 prompt,在 LangSmith 中对比两组反馈分数。

代码示例:用评估器给 prompt 打分

from langsmith import Client
from langsmith.evaluation import evaluate

client = Client()
results = evaluate(
    lambda inputs: agent_executor.invoke(inputs),
    data="my-test-dataset",   # 预先准备好的数据集
    evaluators=[correctness_evaluator, conciseness_evaluator],
    experiment_prefix="prompt-v2",
)

跑完后,你可以在实验页面上看到新版 prompt 在“正确性”上提升了 8%,但“响应速度”下降了 5%,接着你就可以决定是否接受这个 trade-off。

▎💰 成本分析:把每一分钱都算清楚

LLM 调用是烧钱的,尤其是 Agent 动不动就一次请求调用十几次模型。LangSmith 让成本可视化:

  • 每个 Run 都记录了精确的 token 用量,你可以用 client.list_runs(project_name="prod") 拉取一段时间内的所有 runs,按天聚合,输出报表。

  • 你还可以按 模型 分组,看到是 gpt-4o 花得多,还是 claude-3 花得多;按 工具 分组,看到哪个工具链最耗 token。

  • 如果想实时监控,LangSmith 的 Webhook 或 Python SDK 可以定期拉取数据,当单日花费超过阈值时告警。

示例:拉取昨天所有成功 Run 的总花费

from langsmith import Client
import datetime

client = Client()
runs = client.list_runs(
    project_name="prod-agent",
    start_time=datetime.datetime.now() - datetime.timedelta(days=1),
    error=False,
)
total_cost = sum(run.total_cost for run in runs if run.total_cost)
print(f"昨日成本: ${total_cost:.2f}")

这让你在月结账单出来之前就能知道钱花在了哪。

🧠 面试官最想听的总结 “我用 LangSmith 把 Agent 的问题定位、Prompt 迭代和成本管理串成了一个闭环:出问题→看 Trace 定位→优化 Prompt 并版本化→评估实验→上线并监控成本。这样效率比盲改高出几个数量级,还能用数据说服团队。”


🚨 3. Agent 在生产环境偶发性输出异常,如何用 LangSmith 系统性排查?

生产环境的偶发 bug 最让人头疼,但 LangSmith 给了你一套“从粗到细”的排查方法论,配合定制化元数据,能快速圈定病灶。

▎🕵️ 第一步:快速检索,圈定异常样本

生产环境每天可能有上万个 Trace,你不可能手动翻。必须靠过滤和搜索。

  • 如果你在代码里为每个请求注入了 metadata(比如 {"environment": "prod", "session_id": "abc123", "feature": "order_agent"}),就可以在 UI 里用类似 metadata.environment = prod AND feedback.score < 0.5 的条件快速筛选。

  • 也可以用 Python SDK 编程筛选:

runs = client.list_runs(
    project_name="prod",
    filter='eq(metadata.environment, "prod")',
    is_root=True,
    error=None  # 可以先拉所有 root run
)

🔬 第二步:给 Trace 增加“诊断信息”

偶发问题往往和特定输入、特定上下文有关。靠裸 trace 有时不够,你需要在应用代码里主动挂载更多信息到 Run。

  • 利用 langsmith.run_helpers@traceable 装饰器,自定义子 run,把关键中间变量暴露出来。

  • 对异步调用或特殊分支,手动添加 run.add_metadata({"user_region": region, "prompt_version": "v3"})

from langsmith import traceable

@traceable(run_type="tool")
def my_custom_tool(query: str):
    # 工具逻辑
    result = ...
    return result

这样一旦某类地区用户的请求出了问题,你就能直接从 metadata 里定位。

▎🔍 第三步:深度解剖一个异常 Trace

找到典型异常 Trace 后,从根节点往下一层层看:

  1. 看最终的输出异常:是格式错误,还是内容胡言乱语?

  2. 回溯 LLM 调用:检查模型接收的完整 prompt。特别留意 RAG 检索来的上下文,可能混入了脏数据或与问题完全无关的文档。

  3. 检查工具调用序列:Agent 是否陷入了错误工具的死循环?是否某次工具调用返回了异常(如超时、权限错误),而 Agent 没有做异常处理,直接当成正常信息继续推理?

  4. 对比正常 Trace:找一条同类请求的成功 Trace,对比每一步输入输出的差异。很多时候你会发现,异常 trace 的某个检索步骤少返回了一条关键信息,或者工具调用的参数顺序错了。

▎🧪 第四步:将异常案例固化为回归测试

找到根因后,别修复了就完了。把这次出错的输入输出保存成数据集中的一个示例。

from langsmith import Client

client = Client()
dataset = client.create_dataset("agent-regression-tests")
client.create_example(
    inputs={"input": "导致异常的原始用户问题"},
    outputs={"expected_output": "期望的正确答案或行为"},
    dataset_id=dataset.id,
)

之后每次修改 Agent(比如更新 prompt 或工具),都跑一遍这个回归测试集,确保旧问题不再复现。还能结合 CI/CD 流水线,把评估设为部署门禁。

▎⚙️ 第五步:设置在线监控与预警

被动排查永远慢半拍,LangSmith 允许你添加在线评估器,实时给每个 Run 打分。比如,用一个规则引擎检查输出是否包含特定错误关键词,或者用一个轻量模型判断是否拒绝回答。一旦某条 Run 分数低于阈值,LangSmith 可以触发 Webhook 发送告警到你的 Slack 或钉钉。

🧠 这套系统化排查思路,面试说出来就是架构级的回答 “偶发异常不可怕,可怕的是没有一套从检索、诊断、复现到回归的闭环。LangSmith 帮我构建了这样的能力:通过 metadata 快速筛选异常样本,利用 Trace 深度解剖根因,修复后同步创建数据集防止回归,最后加上在线评估器主动预警。这样团队就从‘被动救火’转向了‘主动防御’。”


最后总结一句:LangSmith 不只是一个 trace 查看器,它本质上是一套 LLM 应用的全生命周期可观测性和质量保障平台。面试中你能把“用 Trace 排查问题”升维到“用 LangSmith 构建质量闭环”,基本就锁定了高评价。

Agent 健壮性与高级优化

DSPy 的自动化 Prompt 优化原理


1、DSPy 解决了什么问题?

难度级别:⭐(考察要点:手动 Prompt 工程的痛点、自动化优化的核心思路)

传统 Prompt 工程本质上是"手动梯度下降",每次模型版本更新或任务变化都要人工重新调整 Prompt,既费时又不可复现。DSPy 把 LLM 程序变成可编译、可优化的模块化系统,用优化器自动搜索最优的 Prompt 配置,将 Prompt 工程从手工艺变成可量化、可迭代的工程实践。


2、:DSPy 的 Signature、Module、Optimizer 三个核心概念是什么,BootstrapFewShot 和 MIPRO 的工作原理有何不同?

难度级别:⭐⭐⭐(考察要点:Signature 声明 IO 契约、Module 组合多个 Predict/CoT、BootstrapFewShot 自举 few-shot、MIPRO 贝叶斯搜索 instruction)

1️⃣ Common Answer

Signature 定义输入输出类型,Module 是把多个 LLM 调用组合在一起的模块,Optimizer 负责自动找最好的 Prompt。BootstrapFewShot 会从训练数据里自动生成 few-shot 示例,MIPRO 用更复杂的算法优化 Prompt。

2️⃣ Impressive Answer

我会从 3 个角度来回答:

  1. 首先说 Signature 的核心设计理念。Signature 是声明式的 IO 契约,你只描述"做什么"(输入是什么、输出是什么),不写具体 Prompt 措辞。实际的 Prompt 构造由 DSPy 在编译时自动完成,并且可以被优化器修改。这个设计的价值是把"程序逻辑"和"Prompt 表达"解耦,类比编程里分离接口和实现。

  2. 其次说 Module 的组合能力。Module 是 DSPy 中类比神经网络层的概念,内置 dspy.Predict(基础调用)、dspy.ChainOfThought(自动加入 reasoning 字段激活 CoT)、dspy.ReAct(内置 ReAct 循环)。可以把多个 Module 组合成复杂程序,比如 RAG Pipeline 里组合 Retrieve 和 ChainOfThought,整个 pipeline 作为一个整体被优化器优化。

  3. 最后说两种 Optimizer 的原理差异。BootstrapFewShot 是自举优化:让 Module 对训练集跑推理,把通过评估指标的样本收集起来作为 few-shot 示例注入 Prompt,本质是"让模型先试,把成功案例加回 Prompt"的迭代。实现简单、成本低,适合快速迭代。MIPRO(Multi-prompt Instruction PRoposal Optimizer)更进一步,不只优化 few-shot 示例,还会优化 Signature 的 instruction 部分(告诉 LLM 怎么做任务的措辞),用贝叶斯优化在 Prompt 空间搜索,比暴力搜索高效,适合对性能要求更高、可接受更多计算成本的场景。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
技术深度 描述功能层面,未解释底层原理 解释了 BootstrapFewShot 的自举机制和 MIPRO 的贝叶斯搜索原理
实践经验 无代码示例和适用场景分析 分析了 DSPy 适用场景(有评估指标+有训练数据)和局限性
思考维度 将 DSPy 定位为"自动写 Prompt 的工具" 理解"可编译 LLM 程序"的核心设计哲学,类比神经网络优化
给面试官的印象 知道 DSPy 存在但没深入研究 有深入原理理解,能判断在什么场景引入 DSPy

3、团队的 RAG 问答系统 Prompt 很难手动调优,考虑引入 DSPy,需要满足什么前提条件?

难度级别:⭐⭐⭐(考察要点:评估指标可量化、训练数据规模、优化计算成本、适用场景判断)

1️⃣ Common Answer

引入 DSPy 需要有训练数据,还要定义评估指标,然后让优化器自动跑就行。

2️⃣ Impressive Answer

引入 DSPy 有几个关键前提要先评估,不满足的话引入反而是浪费。

第一,必须有可量化的评估指标。DSPy 优化器靠指标函数判断哪个 Prompt 更好,如果你的任务是创意写作、开放对话这种难以量化的场景,DSPy 发挥不了作用。RAG 问答场景通常可以用 Answer Correctness、Faithfulness 等指标,配合 LLM-as-Judge 实现,这个条件满足。

第二,需要一定规模的训练数据。至少几十个有标注的问答对,用于 BootstrapFewShot 的自举和优化效果评估。如果数据太少,优化器的搜索空间不足,结果不可信。

第三,要接受更高的优化计算成本。优化过程需要多次调用 LLM(尤其是 MIPRO),成本比手动调 Prompt 高很多。如果任务是高频、对延迟敏感的实时接口,这个成本可能不可接受,换成离线批量优化后部署固定 Prompt 会更合适。满足这三个条件,引入 DSPy 的收益才能超过成本。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
技术深度 给出引入步骤,无前提条件分析 从评估指标、数据规模、计算成本三个维度给出前提判断
实践经验 无局限性认知 明确指出 DSPy 不适合的场景(创意任务、无评估指标)
思考维度 "引入 = 更好" 引入框架要先评估 ROI,成本不合适宁可不引入
给面试官的印象 知道 DSPy 能用 有完整的技术选型判断力,理解工具适用边界


从零构建轻量级 Agent 的核心组件设计


1、ReAct Agent 的核心循环逻辑是什么?

难度级别:⭐(考察要点:Thought-Action-Observation 循环、终止条件、工具注册)

ReAct Agent 的核心是一个循环:让 LLM 输出 Thought(思考过程)和 Action(调用哪个工具、传什么参数);执行工具得到 Observation(结果);把 Observation 加入对话历史,再次调用 LLM;重复直到 LLM 输出 Final Answer。工具用字典注册(key 是工具名,value 是函数),循环靠 max_steps 上限防止死循环。


2、如何不依赖任何框架从零实现一个 ReAct Agent,工具注册与动态调用如何设计,何时应该引入框架?

难度级别:⭐⭐⭐(考察要点:装饰器+字典的工具注册模式、Prompt 格式设计、输出解析、框架引入复杂度阈值)

1️⃣ Common Answer

从零实现 Agent 就是写一个循环,让 LLM 决定调哪个工具,执行完把结果给 LLM,一直到任务完成。工具用字典存起来,key 是工具名,value 是函数。复杂了再引入框架。

2️⃣ Impressive Answer

我会从 3 个角度来回答:

  1. 首先说工具注册系统的设计。用装饰器 + 字典的模式:定义一个全局 _TOOL_REGISTRY 字典,写一个 @tool 装饰器,被装饰的函数自动注册进去,存函数引用、description 和参数 schema。调用工具时从字典里取函数动态执行:_TOOL_REGISTRY[tool_name]["function"](**tool_input)。这个模式的优点是注册和调用完全解耦,新增工具只需加装饰器,主循环代码不需要改。

  2. 其次说 ReAct 循环的关键工程细节。Prompt 设计要引导 LLM 输出结构化的 Thought / Action / Action Input 格式;输出解析用正则匹配提取工具名和参数;stop=["Observation:"] 让 LLM 在该停的地方停,等我们填充工具结果再继续。边界情况要处理:JSON 解析失败、工具不存在、达到 max_steps 上限。这些边界情况不处理,Agent 在真实环境里必然频繁崩溃。

  3. 最后说引入框架的判断标准。当需求超出以下阈值时,自建的维护成本就超过了引入框架的学习成本:需要流式输出 Token、需要对话状态持久化、需要多 Agent 并发编排、需要 Trace 可观测性、需要复用工具生态。超过 2-3 个需求就该引入 LangChain/LangGraph。但如果是单一、高频、对延迟敏感的场景(内容审核、简单分类),保持自建轻量实现往往更好。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
技术深度 描述思路但无可运行代码 给出装饰器注册系统和 ReAct 循环的完整设计,含边界处理
实践经验 未涉及输出解析、错误处理等工程细节 处理了 JSON 解析失败、工具不存在、max_steps 等边界情况
思考维度 "复杂了就引入框架"的模糊判断 给出具体的复杂度阈值,量化引入框架的触发条件
给面试官的印象 理解 Agent 概念但未实际动手写过 从零实现过 Agent,理解框架底层机制,有清晰的技术选型判断力

3、自建 Agent 在生产环境中遇到 LLM 输出格式不稳定导致解析失败,如何处理?

难度级别:⭐⭐⭐(考察要点:输出解析容错、structured output / function calling 的替代方案、降级策略)

1️⃣ Common Answer

可以在解析失败时让 LLM 重新输出,在 Prompt 里提示它按正确格式来,或者多试几次。

2️⃣ Impressive Answer

输出格式不稳定是自建 Agent 最常见的生产问题,有两个层次的解法。

第一,从根本上用 Structured Output 代替 Prompt 约束格式。OpenAI 的 response_format={"type": "json_schema", ...} 或者 Function Calling 机制,能在模型层保证输出格式,从根本上消除正则解析失败的问题。这是最推荐的方案,把工具调用的参数解析交给模型层保证,不依赖脆弱的正则匹配。

第二,如果必须用文本解析,加好兜底逻辑。解析失败时给 LLM 发一条修正消息"请按规定格式输出",最多重试 2 次;连续失败后返回降级回复,而不是让调用方看到异常。同时把解析失败的原始输出记录下来,用于后续优化 Prompt 或 stop sequence 配置。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
技术深度 靠 Prompt 约束格式 指出 Structured Output/Function Calling 是根本解法
实践经验 无降级策略 给出完整的容错链路:重试 → 降级 → 记录日志
思考维度 被动修复 主动用模型层能力消除解析不稳定的根本原因
给面试官的印象 知道基本处理方式 有生产环境 Agent 可靠性建设的完整经验