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_id、session_id、feature_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,你就会看到一条包含 AgentExecutor → LLM → Tool 等层层嵌套的 Trace。点击任何一个 Run,prompt 原文、工具返回的 JSON、模型思考过程一览无余。
🧠 面试这样答直接拉开差距 “Trace 本质是 LLM 应用的可观测性基石,它把一次请求的所有 LLM 和工具交互结构化成一棵树。有了它,我们才能做后续的调试、评估、成本分析和回归测试,而不是对着控制台打印的零散日志瞎猜。”
🩺 2. 如何用 LangSmith 定位 Agent 问题,并做 Prompt 版本优化和成本分析?¶
这道题面试官在考察你是不是真的用过 LangSmith 做工程优化,而不是只会点开 UI 看错误。我们分三块来讲。
▎📍 问题定位:像法医一样解剖 Agent 的每一步¶
Agent 常见的问题:调错工具、循环死锁、幻觉答案、输出格式异常。用 LangSmith 定位的常规路径如下:
-
过滤出问题会话 如果线上有用户反馈,直接根据
run_id或自定义的metadata.user_id搜索 Trace。没有反馈也没关系,可以通过 LangSmith 的过滤器,找出所有包含Error状态或feedback.score = 0的 Run。 -
展开 Trace 树,逐节点排查 打开一个异常 Trace,从左边的树形结构开始:
- 根节点看总延迟和总 token 是否异常。
- 进入第一个 LLM Run,检查 输入 prompt 是否包含了正确的上下文。有时 RAG 检索回来的文档根本不相关,模型自然答非所问。
- 查看 输出,如果模型的输出本该是函数调用,却给了自然语言,说明 prompt 约束不够或模型本身不稳定。
-
再进入工具 Run,检查传入的参数是否符合预期,工具返回的错误信息是不是被模型误吞了。
-
利用“对比模式” LangSmith 允许你选中两次 Run 进行左右对比,比如一次成功、一次失败。你可以直观看到,到底是哪个步骤的输入产生了偏差,比如检索结果的排序不同,导致模型没读到关键段落。
实战技巧:我通常会在 Agent 的每个关键节点加一个 with langsmith.trace 上下文,手动注入一些调试信息,或者用 run.collect 把中间变量挂到 trace 上,这样排查效率更高。
▎📝 Prompt 版本优化:告别“凭感觉改词”¶
Prompt 优化如果不做版本管理,基本等于玄学。LangSmith 的 Hub 功能就是解决这个的。
工作流:
- 把 prompt 推送到 Hub,每次修改都会自动生成 commit,像 Git 一样。
-
跑一批测试用例,关联到不同 prompt 版本。可以使用 LangSmith 的数据集功能,把用户历史问题沉淀成评估集。
-
用
langsmith.evaluation里的评估器,或者自己写一个,对每个 prompt 版本的输出做自动打分(如正确性、相关性、格式规范)。 -
在线下比较不同版本的平均分和个案表现,选出最优版本后再上线。你甚至可以做 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 后,从根节点往下一层层看:
-
看最终的输出异常:是格式错误,还是内容胡言乱语?
-
回溯 LLM 调用:检查模型接收的完整 prompt。特别留意 RAG 检索来的上下文,可能混入了脏数据或与问题完全无关的文档。
-
检查工具调用序列:Agent 是否陷入了错误工具的死循环?是否某次工具调用返回了异常(如超时、权限错误),而 Agent 没有做异常处理,直接当成正常信息继续推理?
-
对比正常 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 个角度来回答:
-
首先说 Signature 的核心设计理念。Signature 是声明式的 IO 契约,你只描述"做什么"(输入是什么、输出是什么),不写具体 Prompt 措辞。实际的 Prompt 构造由 DSPy 在编译时自动完成,并且可以被优化器修改。这个设计的价值是把"程序逻辑"和"Prompt 表达"解耦,类比编程里分离接口和实现。
-
其次说 Module 的组合能力。Module 是 DSPy 中类比神经网络层的概念,内置
dspy.Predict(基础调用)、dspy.ChainOfThought(自动加入 reasoning 字段激活 CoT)、dspy.ReAct(内置 ReAct 循环)。可以把多个 Module 组合成复杂程序,比如 RAG Pipeline 里组合 Retrieve 和 ChainOfThought,整个 pipeline 作为一个整体被优化器优化。 -
最后说两种 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 个角度来回答:
-
首先说工具注册系统的设计。用装饰器 + 字典的模式:定义一个全局
_TOOL_REGISTRY字典,写一个@tool装饰器,被装饰的函数自动注册进去,存函数引用、description 和参数 schema。调用工具时从字典里取函数动态执行:_TOOL_REGISTRY[tool_name]["function"](**tool_input)。这个模式的优点是注册和调用完全解耦,新增工具只需加装饰器,主循环代码不需要改。 -
其次说 ReAct 循环的关键工程细节。Prompt 设计要引导 LLM 输出结构化的
Thought / Action / Action Input格式;输出解析用正则匹配提取工具名和参数;stop=["Observation:"]让 LLM 在该停的地方停,等我们填充工具结果再继续。边界情况要处理:JSON 解析失败、工具不存在、达到 max_steps 上限。这些边界情况不处理,Agent 在真实环境里必然频繁崩溃。 -
最后说引入框架的判断标准。当需求超出以下阈值时,自建的维护成本就超过了引入框架的学习成本:需要流式输出 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 可靠性建设的完整经验 |