LangSmith 追踪实战:判断花 4.4 秒,回答花 1 秒,最后一句是"我无法访问"

我问了它一句:我的投资组合中科技股占比是多少?

选的是进取型那个客户。300 万,股票 65%、债券 15%、现金 5%、另类 15%。

跑完终端打印"处理用时 6.54 秒"。我当时的反应是:还行吧,六秒多,能接受。

然后我打开 LangSmith,点了下这条 trace。

6.43 秒。比终端那个数少 0.11 秒,是本地 print 的开销。

两个数差不多。但拆开看是另一回事。

4.38 秒判断该不该快,1.06 秒回答

Waterfall 展开是这样:

MD 复制代码
assess 4.38s

Tongyi 2.73s

JsonOutputParser 0.00s

reactive 1.06s

Tongyi 1.05s

respond 0.00s

判断该不该快,4.38 秒。真正回答,1.06 秒。

四比一。

代码里,反应式那层写的是"毫秒级响应,直接调数据和预设回答"。实测 6.54 秒,其中一秒在回答。

为了决定"要不要快",花了四倍于"快"本身的时间。

如果分流规则写成正则,查询里出现"指数""点位""行情"就走反应式,这一步是 0 毫秒。

用一次 LLM 调用去判断该不该走轻量路径,本身就贵。这钱在普通查询里不显眼,落在"紧急查询"上就有点怪:用户要的是快,系统先花四秒想清楚要不要快。

还有个细节,是拿计算器减出来的。

assess 节点 4.38 秒。下面挂着两个子 span:Tongyi 2.73 秒,JsonOutputParser 0.00 秒。

我一开始以为这俩加起来就是 4.38。减了一下,2.73。差 1.65 秒。

这 1.65 秒去哪了?

trace 下面有行小字:Some runs have been hidden. Show 6 hidden runs。

点开那六个,就是这 1.65 秒------prompt 拼装、状态序列化、回调打点,还有几个内部 step。

一个节点的耗时,跟它里面那次 LLM 调用对不上。中间夹着的东西平时看不见。

不展开这六个 span,很容易以为 4.38 秒全是模型在思考。实际模型只占 62%。

这个差在调优的时候有区别。想让它快,改 prompt 只能动 2.73 秒那一块,剩下 1.65 秒是框架自己的开销,改 prompt 改不掉。

trace 顶部还有个 Trajectory 切换。Waterfall 按时间轴摊开,Trajectory 按动作顺序排。

这次的 Trajectory 只有三行:assess、reactive、respond。

三行,一次完整的用户交互。看着挺干净的。

我当时也是这么觉得的。

协调层只收到一句话

assess_query 里,喂给 LLM 的东西是这样的:

python 复制代码
input_data = {

    "user_query": state["user_query"],

}

一个字段。就一句话。

ASSESSMENT_PROMPT 里对应的占位符也只有 {user_query}。客户画像在 state 里躺着,没被传进去。

所以协调层判断"这个问题复杂不复杂"的时候,它不知道这个客户是谁、有多少钱、配了什么。

它看到的只有七个字:我的投资组合中科技股占比是多少?

然后它输出了这么一段理由:

该查询要求实时获取用户当前投资组合中科技股的持仓占比,属于账户/持仓状态类事实性信息,不涉及复杂推理或外部知识调用,需即时从用户持仓数据中提取并计算,符合紧急、直接、低延迟响应特征。

"需即时从用户持仓数据中提取并计算。"

问题在于,这个 Agent 没有这个能力。

reactive_processing 里注册的工具就一张表:

python 复制代码
tools_map = {

    "上证指数查询": query_shanghai_index,

}

一个。查上证指数的。

没有工具能读用户持仓。

它凭一句话的措辞,推断出了一个不存在的能力,然后拿这个能力做了分流决策。

这段理由写得挺好。识别出是"账户/持仓状态类事实性信息",判断出"不涉及复杂推理",推出"符合紧急、直接、低延迟响应特征"。

逻辑链完整,每一步都站得住。

除了第一步。

而这段理由,LangSmith 上看不到。它只活在终端的 print 里。

trace 上 assess 这个 span,Input 只有一个 user_query,Output 只有 processing_mode 和 query_type 两个字段。中间那两百字推理,LangSmith 不知道。

除非你把 reasoning 也塞进 state,让它跟着 trace 走。

画像就在同一个 state 里,它说访问不到

客户是 customer2。进取型,长期,目标是"财富增长"和"资产配置多元化",偏好新兴市场和高成长行业,组合 300 万。

配置:

python 复制代码
"current_allocations": {

    "股票": 0.65,

    "债券": 0.15,

    "现金": 0.05,

    "另类投资": 0.15

}

股票 65%。

这些在 state 里。但反应式那条路的 prompt 里,没有 {customer_profile} 这个占位符。

python 复制代码
REACTIVE_AGENT_PROMPT = """你是一个财富管理投顾AI助手,请根据用户的问题回答。

  


你可以使用以下工具:

- 上证指数查询: 用于查询上证指数的最新行情

  


如果用户询问上证指数相关信息,请在回答中包含 [CALL_TOOL:上证指数查询] 标记。

如果不需要使用工具,直接回答用户问题即可。

  


用户问题: {input}

  


请提供专业、简洁、准确的回答。

"""

只有 {input}。

所以它说"我无法访问您的投资组合数据"的时候,是真的没有数据。

LangSmith 的 Output 面板上,这两行是挨着的:

表格

字段 值
final_response 我无法访问您的投资组合数据,因此无法确定科技股的具体占比...
customer_profile {7 items}

同一本 state 字典,上下隔了三行。一行说我访问不到,一行装着他全部的家底。

这两行摆在一起,比看十遍 prompt 都直观。

严格说,就算把画像传进去,它也答不了"科技股占比"。current_allocations 只到"股票"这一级,没细分到行业。

但它至少能说:"您当前股票仓位 65%,科技股的细分占比不在我可访问的数据范围内。"

两句话对用户的价值差得远。前面那句用了手上的信息,边界划在真该划的地方。后面那句把已知的也一起否了。

上次跑同一个问题,结果不是这样。

那次它先说"在我的知识库中没有关于您具体投资组合中科技股占比的数据",紧接着又补了一句"根据我的经验,部分私募基金对单一行业(如科技)的持仓上限常设为20%-30%"。

边界划窄了,漏了个没依据的数字出去。

这次边界划宽了,把已有的信息也一起关在门里。

同一个问题,两次跑,两次都错,错的方向正好相反。

我一开始以为是 prompt 写得不好,改改就完事了。后来发现不是这么回事。这个 Agent 的"知识边界",每次运行都是临时划的,划在哪儿取决于模型当时怎么想。

判成紧急,紧急通道是空的

Output 里还有个字段:query_type: emergency。

state 定义里有这么一行:

python 复制代码
emergency_response: Optional[Dict[str, Any]] # 紧急响应结果

初始化给了 None。

整个 702 行里,它只出现过这两次。

系统判了个"紧急"出来,而专门留给紧急查询的那个槽,从建好到现在一直是空的。

实际路径是 assess → reactive → respond。判成 informational 或者 analytical,走的也是这一条。区别只在终端里打印的那行字。

所以 query_type 这个三分法,在代码里就是个标签。标签和路由是两回事。

真正决定走哪条路的,只有 processing_mode 一个字段。

这点我当时没意识到。看到 query_type 是 emergency,我还以为走了什么专门的紧急通道。

current_phase 从头到尾没动过

Input 面板上 current_phase 是 assess。Output 面板上,还是 assess。

6.43 秒,三个节点跑完,这个字段一动没动。

它不是摆设。state 里它归在"控制流"那一组。深思熟虑那条路的节点都在维护它------collect_data 成功设成 "analyze",出错设回 "collect_data";analyze 成功设 "recommend",出错设回 "analyze"。

一套完整的"下一步该去哪"的指针。

但 assess_query 没设它,reactive_processing 也没设。

走快路的时候,这指针从头到尾停在入口。

真要在中途"回到某个阶段重来",反应式这条路没有位置可回。它不知道自己刚才在哪。

上一个 case 那个投研 Agent 是另一个极端:current_phase 每个节点都在更新,出错还写回上游,可图是直线,写回去了没人接。

一个把位置写进了状态,图却是直线。另一个图里有条件边,位置这个字段干脆没人管。

两个都没走完。

两条虚线,一个黑箱

跑之前,LangGraph 把流程图打印成了 Mermaid:

js 复制代码
__start__ --> assess;

assess -.-> collect_data;

assess -.-> reactive;

collect_data --> analyze;

analyze --> recommend;

recommend --> respond;

reactive --> respond;

respond --> __end__;

注意 assess 那两条边,箭头是 -.->,虚线。

实线是固定边,虚线是条件边。对应的代码:

python 复制代码
workflow.add_conditional_edges(

    "assess",

    lambda x: "reactive" if x.get("processing_mode") == "reactive" else "collect_data",

    {

        "reactive": "reactive",

        "collect_data": "collect_data"

    }

)

整张图就这两条虚线。整个架构的赌注,也全在这两条线上。

直线的问题是退不回去,分叉的问题是有可能走错。

走哪条,取决于 assess 那一步的输出。而那一步的输入只有一句话。

Mermaid 画的是"可能走哪几条路",trace 画的是"这次实际走了哪条"。

两张图放一起才完整。Mermaid 上八个节点八条边,这次走过的只有三个。

差的那部分,是没被触发的分支。它们还在,只是没走到。

兜底逻辑全指向"快",而 trace 上看不出区别

assess_query 里有一段异常处理:

python 复制代码
processing_mode = result.get("processing_mode", "reactive")

if processing_mode not in ["reactive", "deliberative"]:

processing_mode = "reactive" # 默认使用反应式处理

  


query_type = result.get("query_type", "emergency")

if query_type not in ["emergency", "informational", "analytical"]:

query_type = "emergency" # 默认为紧急查询

取不到值,走反应式。值不合法,还是走反应式。

查询类型同理,取不到当紧急,不合法也当紧急。

两个默认值都指向最快的那条路。

这么写不算错。宁可快而糙,不要慢而卡。大多数查询确实简单,兜底走快路,平均体验更好。

问题在它是隐式的。

LLM 输出合法值,流程正常走;LLM 输出崩了,兜底走快路,日志里看不出区别------因为兜底之后的 processing_mode 也是 "reactive",跟正常判出来的一模一样。

LangSmith 上更看不出来。这个 span 的 Output 就是 reactive,它不会标注"这是兜底兜的"。

除非你在兜底那个分支里加个 tag,比如 ls_tags=["fallback"]。

不然这个信息在整条链路上就没了,事后查不出来。

标签和元数据是动态注入的,这点做得挺不错

这条 trace 上挂了四个 tag:

customer-customer2、hybrid-agent、wealth-advisor、进取型

最后那个不是写死的。代码里这么来的:

python 复制代码
tags=[

    f"customer-{customer_id}",

    "hybrid-agent",

    "wealth-advisor",

    customer_profile.get("risk_tolerance", "unknown")
]

客户画像里的 risk_tolerance 直接变成 tag。

这样在 LangSmith 上就能按"进取型"筛,把这类客户的调用全拎出来看。也能按客户 ID 筛。业务代码一行没动。

metadata 里还挂着 customer_id、investment_horizon。另外两个 ls_integration 和 ls_run_depth 不是我加的,是 LangSmith 自己补的,用来标这条链路是怎么接进来的。

这块用着确实顺。LangChain 和 LangSmith 是一家的,RunnableConfig 里塞的东西框架直接吞了,节点进出自动打点。

换个框架就得自己一项项埋。

prompt 还硬编码在 py 文件里,这一步可以挪上去

查出来的这几处,最好改的是 REACTIVE_AGENT_PROMPT 少了 {customer_profile}。

加个占位符,把画像拼进去,两行代码。

改完怎么知道真的好了?

现在只能靠肉眼看。跑一遍,觉得"嗯,这次说了 65%",就算过了。

LangSmith 的 Prompts 面板能把这个流程规范一点。prompt 传上去,起个名,改一版存一版,每次都有记录。

跑测试的时候用 experiment_prefix 区分版本:

python

1

2

experiment_prefix="prompt-v2-with-profile"

Experiments 页面上会并排列出 v1 和 v2 的结果,一个用例一行,谁好谁坏一眼能看出来。

不用自己写脚本对比,也不用记上次跑的是哪一版。

代价是 prompt 得从代码里挪出去。本地调试多一次网络请求,改个词要开网页。

自己一个人写的时候这套有点重。但要是 prompt 会反复调,或者不止一个人动它,早晚得挪上去。

一条 trace 能做的最后一件事,是变成一条测试样本

LangSmith 右上角有个 +,点开是 Add Example to dataset。

Inputs 自动填好了,就是这次的 user_query 和 customer_id。

Reference Outputs 那栏空着,留给你写。

如果我写"回答应至少说明客户当前股票仓位为 65%,并明确科技股细分占比不可得",那这条样本就永远盯着这个毛病。下次改完 prompt 再跑,它要是又冒出一句"我无法访问",对比一下就知道。

存得多了,这个 Dataset 就有点像把尺子。

界面上管它叫"黄金测试集"。

黄金这两个字,工具给不了。得你自己一条条挑。

它只能把一条 trace 变成带参考答案的样本。参考答案那一栏,是空的。

最后

左侧的 Status 筛选里,这条 trace 是 success。绿勾。

没有 error,没有 interrupted,没有 pending。

六秒半,三个节点,一次完整的调用。从系统的角度看,这是一次干净的、成功的执行。

用户那边不是。

LangSmith 在这条链路上干的事,说到底就一件:把一次黑箱调用拆成输入和输出。

拆开之后才看得见 4.38 和 1.06 那个比例。也才看得见 final_response 和 customer_profile 挨在一起的那种别扭。

还有 current_phase,6.43 秒里一动没动。

这些东西在终端日志里都没有。日志只会告诉你进入了 assess、进入了 reactive、处理用时 6.54 秒。

它不告诉你这些时间花得值不值。

也不告诉你那个 reactive 是判断出来的,还是兜底兜的。

LangSmith 能回答发生了什么。应不应该,它答不了。

后面那半截得靠另一套东西:评估器、测试集、评分。

那是另一个 case,单独写一篇。

不过有个问题可以先放着:这次的回答,丢进评估器会得多少分?

模式判的 reactive,符合"紧急查询"的定义。回答里"科技股""占比"也都在。

我猜分数不会低。

可用户一个字的有用信息都没拿到。

相关推荐
云和恩墨1 小时前
数据库监控简史:从会回答到能交付——AI-DBA如何成为真正的数字同事
数据库·人工智能·dba
u1301301 小时前
GitHub 热榜项目:日榜(2026-11-02)
人工智能·github
米小虾1 小时前
Agent 省钱工程:七成的钱不是模型花掉的
人工智能·agent
丘山录1 小时前
本地部署 LongCat-2.5,要 7 台机器我只有 4 台
人工智能·算法
张炳午1 小时前
DeepSeek 开源 Super-agent 原型拆解:授权链怎么设计、实验怎么读、Watchdog 为什么没实装
人工智能
方方洛1 小时前
ray教程-23-附录C-术语表
人工智能·分布式·机器学习
用户94658117999181 小时前
推理 INFRA 方法论(5):MoE 推理:Batch、DeepEP、DeepGEMM 与负载均衡
人工智能
用户94658117999181 小时前
推理 INFRA 方法论(2):分布式推理:TP、DP、EP、CP 与 PP 的取舍
人工智能
用户94658117999182 小时前
推理 INFRA 方法论(1):通信模型:从集合原语到跨机拓扑
人工智能