Function Calling 调用链追踪:从 Prompt 参数到执行的全流程可视化
作者:码龙大大
Function Calling 的链路追踪,核心就是把一次工具调用从 Prompt 输入到最终结果返回的全过程,拆解成几个关键节点并记录下来,让每一步都可见、可查、可回溯。下面把这条链路和具体实现方式拆开讲清楚。
如何实现链路追踪
实现方式主要分两类,可以根据实际场景选:
- 侵入式埋点:在代码关键位置手动插入 Recorder,记录输入、输出和耗时。实现简单直观,但代码侵入性强。
- 无侵入式 Hook(推荐):利用 OpenTelemetry SDK 等工具,在 HTTP 层或 SDK 层拦截请求,自动注入
trace_id和span_id,不修改原有业务代码。
关键实现细节:
- 用
trace_id关联一次完整调用,用span_id关联链路中的每个具体步骤。 - 跨服务调用时,需要将 Trace 上下文透传给下游服务,保证链路完整。
- 对流式响应(SSE),需要延迟关闭 Span,避免 Trace 断裂。
一、当 Agent 告诉你"我已经调用了搜索",但实际没有
一个常见的场景:用户问 Agent 某个问题。
Agent 回复"我已查询了最新数据,结果是……"
但实际上,工具调用根本没有发生。
大模型凭空生成了结果,即幻觉。
排查这类问题极其困难。
你无法知道模型是否真的产生了 tool_call。
即便产生了,参数是否正确传递给了工具。
即便正确传递了,工具返回的结果是否被模型正确消费。
每一步都是黑盒,只能靠猜。
核心诉求很明确。
需要看到 Function Calling 的完整调用链路。
从用户输入 Prompt,到模型输出 tool_call 参数。
再到参数注入工具执行,最后结果回传给模型。
二、Function Calling 的请求-执行链路
Function Calling 的调用包含四个关键节点。
第一,Prompt 编译:将用户输入和工具 Schema 组装为模型输入。
第二,模型推理:模型决定调用哪个工具及参数。
第三,工具执行:解析参数,调用实际函数。
第四,结果注入:工具返回结果写入对话上下文。
sequenceDiagram
participant U as 用户
participant P as Prompt 组装层
participant M as LLM
participant T as 工具调度器
participant F as 实际函数
participant L as 追踪系统
U->>P: 发送问题
P->>P: 注入工具 Schema
P->>L: 📝 Node1: Prompt 快照记录
P->>M: 发起推理请求
M->>L: 📝 Node2: 模型输出(含 tool_call)
M-->>P: 返回 tool_call 决策
alt 需要调用工具
P->>T: 派发 tool_call
T->>T: 参数校验
T->>L: 📝 Node3: 工具名 + 参数
T->>F: 执行函数
F-->>T: 返回结果
T->>L: 📝 Node4: 工具返回值
T-->>P: 注入结果到上下文
else 直接回复
M-->>U: 返回文本回复
end
P->>L: 📝 Node5: 最终响应
P-->>U: 返回给用户每个节点记录三类信息。
时间戳、输入输出快照、延迟耗时。
这些数据构成完整的调用链。
三、链路追踪的 Go 实现
package tracing
import (
"context"
"encoding/json"
"fmt"
"time"
)
// TraceNode 追踪节点
type TraceNode struct {
ID string `json:"id"`
Type string `json:"type"` // prompt_compile, llm_call, tool_exec, result_inject
Timestamp time.Time `json:"timestamp"`
Input json.RawMessage `json:"input"`
Output json.RawMessage `json:"output"`
Duration time.Duration `json:"duration_ms"`
Error string `json:"error,omitempty"`
}
// TraceChain 完整调用链
type TraceChain struct {
TraceID string `json:"trace_id"`
Nodes []TraceNode `json:"nodes"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
// Tracer 链路追踪器
type Tracer struct {
chain *TraceChain
mu sync.Mutex
}
// NewTracer 创建追踪器
func NewTracer(traceID string) *Tracer {
return &Tracer{
chain: &TraceChain{
TraceID: traceID,
Nodes: make([]TraceNode, 0),
StartTime: time.Now(),
},
}
}
// RecordNode 记录一个调用节点,返回结束函数
func (t *Tracer) RecordNode(nodeType string, input interface{}) func(output interface{}, err error) {
inputBytes, _ := json.Marshal(input)
start := time.Now()
node := TraceNode{
ID: fmt.Sprintf("%s-%d", nodeType, len(t.chain.Nodes)),
Type: nodeType,
Timestamp: start,
Input: inputBytes,
}
return func(output interface{}, err error) {
node.Duration = time.Since(start)
if err != nil {
node.Error = err.Error()
} else if output != nil {
node.Output, _ = json.Marshal(output)
}
t.mu.Lock()
t.chain.Nodes = append(t.chain.Nodes, node)
t.mu.Unlock()
}
}
// Export 导出完整链路
func (t *Tracer) Export() *TraceChain {
t.mu.Lock()
defer t.mu.Unlock()
t.chain.EndTime = time.Now()
return t.chain
}在 Function Calling 流程中的使用。
func handleFunctionCall(ctx context.Context, prompt string, tools []Tool) (string, *TraceChain) {
tracer := NewTracer(generateTraceID())
// Node 1: Prompt 编译
endPrompt := tracer.RecordNode("prompt_compile", map[string]interface{}{
"user_input": prompt,
"tool_count": len(tools),
})
compiledPrompt := buildPrompt(prompt, tools)
endPrompt(compiledPrompt, nil)
// Node 2: LLM 推理
endLLM := tracer.RecordNode("llm_call", map[string]interface{}{
"model": "gpt-4",
"prompt_len": len(compiledPrompt),
})
response, err := callLLM(ctx, compiledPrompt)
if err != nil {
endLLM(nil, fmt.Errorf("LLM 调用失败: %w", err))
return "", tracer.Export()
}
endLLM(response, nil)
// Node 3: 工具执行
if response.ToolCall != nil {
endTool := tracer.RecordNode("tool_exec", map[string]interface{}{
"tool_name": response.ToolCall.Name,
"parameters": response.ToolCall.Args,
})
result, execErr := executeTool(ctx, response.ToolCall)
endTool(result, execErr)
if execErr != nil {
return fmt.Sprintf("工具执行失败: %v", execErr), tracer.Export()
}
}
return response.Content, tracer.Export()
}四、追踪系统的边界与成本
全链路追踪带来存储和性能开销。
每个调用链约 2-5KB JSON 数据。
日均 10 万次调用产生约 500MB 追踪数据。
建议设置采样率,生产环境采样 10% 即可。
追踪系统不适合的场景:
延迟极度敏感的实时推理(增加 1-2ms 序列化开销);
本地开发的快速调试(直接用 print 更高效)。
隐私问题需注意。
用户 Prompt 可能包含敏感信息。
追踪数据中应脱敏处理或设置存储过期策略。
五、总结
Function Calling 的链路追踪让每个调用可观测。
通过在关键节点插入 Recorder,捕获输入、输出、耗时。
Tracer 模式比侵入式日志更结构化。
生产环境建议 10% 采样率,配合数据脱敏。
有了完整链路,排查幻觉和参数错误不再靠猜。
到此这篇关于Function Calling 调用链追踪:从 Prompt 参数到执行的全流程可视化的文章就介绍到这了,更多相关Function Calling 链路追踪内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
