feat: 添加对 langfuse 监控的支持 (#242)

* docs: 更新类型检查的 CLAUDE.md

* feat: 添加模型 1M 上下文切换

* chore: remove prefetchOfficialMcpUrls call on startup

* docs: 添加 git commit 规范

* feat: 第一次接入 langfuse

* fix: 修复 generation 的计时的错误

* feat: 添加多 agent 的监控

* feat: 添加 /poor 省流模式,toggle 关闭 extract_memories 和 prompt_suggestion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 修复 lock 文件

* chore: 更新类型依赖

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-11 22:07:38 +08:00
committed by GitHub
parent 6a9da9d546
commit 2fea429dc6
23 changed files with 1242 additions and 6 deletions

View File

@@ -0,0 +1,201 @@
import { startObservation, LangfuseOtelSpanAttributes } from '@langfuse/tracing'
import type { LangfuseSpan, LangfuseGeneration, LangfuseAgent } from '@langfuse/tracing'
import { isLangfuseEnabled } from './client.js'
import { sanitizeToolInput, sanitizeToolOutput } from './sanitize.js'
import { logForDebugging } from 'src/utils/debug.js'
export type { LangfuseSpan }
// Root trace is an agent observation — represents one full agentic turn/session
type RootTrace = LangfuseAgent & { _sessionId?: string }
export function createTrace(params: {
sessionId: string
model: string
provider: string
input?: unknown
name?: string
querySource?: string
}): LangfuseSpan | null {
if (!isLangfuseEnabled()) return null
try {
const traceName = params.name ?? (params.querySource ? `agent-run:${params.querySource}` : 'agent-run')
const rootSpan = startObservation(traceName, {
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
agentType: 'main',
...(params.querySource && { querySource: params.querySource }),
},
}, { asType: 'agent' }) as RootTrace
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
rootSpan._sessionId = params.sessionId
logForDebugging(`[langfuse] Trace created: ${rootSpan.id}`)
return rootSpan as unknown as LangfuseSpan
} catch (e) {
logForDebugging(`[langfuse] createTrace failed: ${e}`, { level: 'error' })
return null
}
}
const PROVIDER_GENERATION_NAMES: Record<string, string> = {
firstParty: 'ChatAnthropic',
bedrock: 'ChatBedrockAnthropic',
vertex: 'ChatVertexAnthropic',
foundry: 'ChatFoundry',
openai: 'ChatOpenAI',
gemini: 'ChatGoogleGenerativeAI',
grok: 'ChatXAI',
}
export function recordLLMObservation(
rootSpan: LangfuseSpan | null,
params: {
model: string
provider: string
input: unknown
output: unknown
usage: { input_tokens: number; output_tokens: number }
startTime?: Date
endTime?: Date
completionStartTime?: Date
},
): void {
if (!rootSpan || !isLangfuseEnabled()) return
try {
const genName = PROVIDER_GENERATION_NAMES[params.provider] ?? `Chat${params.provider}`
// Use the global startObservation directly instead of rootSpan.startObservation().
// The instance method only forwards asType to the global function and drops startTime,
// which causes negative TTFT because the OTel span's start time defaults to "now".
const gen: LangfuseGeneration = startObservation(
genName,
{
model: params.model,
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
},
...(params.completionStartTime && { completionStartTime: params.completionStartTime }),
},
{
asType: 'generation',
...(params.startTime && { startTime: params.startTime }),
parentSpanContext: rootSpan.otelSpan.spanContext(),
},
)
// Propagate session ID to generation span so Langfuse links it correctly
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
if (sessionId) {
gen.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
}
gen.update({
output: params.output,
usageDetails: {
input: params.usage.input_tokens,
output: params.usage.output_tokens,
},
})
gen.end(params.endTime)
logForDebugging(`[langfuse] LLM observation recorded: ${gen.id}`)
} catch (e) {
logForDebugging(`[langfuse] recordLLMObservation failed: ${e}`, { level: 'error' })
}
}
export function recordToolObservation(
rootSpan: LangfuseSpan | null,
params: {
toolName: string
toolUseId: string
input: unknown
output: string
startTime?: Date
isError?: boolean
},
): void {
if (!rootSpan || !isLangfuseEnabled()) return
try {
// Use the global startObservation directly instead of rootSpan.startObservation().
// The instance method only forwards asType and drops startTime,
// causing tool execution duration to be 0.
const toolObs = startObservation(
params.toolName,
{
input: sanitizeToolInput(params.toolName, params.input),
metadata: {
toolUseId: params.toolUseId,
isError: String(params.isError ?? false),
},
},
{
asType: 'tool',
...(params.startTime && { startTime: params.startTime }),
parentSpanContext: rootSpan.otelSpan.spanContext(),
},
)
// Propagate session ID to tool span so Langfuse links it correctly
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
if (sessionId) {
toolObs.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
}
toolObs.update({
output: sanitizeToolOutput(params.toolName, params.output),
...(params.isError && { level: 'ERROR' as const }),
})
toolObs.end()
logForDebugging(`[langfuse] Tool observation recorded: ${params.toolName} (${toolObs.id})`)
} catch (e) {
logForDebugging(`[langfuse] recordToolObservation failed: ${e}`, { level: 'error' })
}
}
export function createSubagentTrace(params: {
sessionId: string
agentType: string
agentId: string
model: string
provider: string
input?: unknown
}): LangfuseSpan | null {
if (!isLangfuseEnabled()) return null
try {
const rootSpan = startObservation(`agent:${params.agentType}`, {
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
agentType: params.agentType,
agentId: params.agentId,
},
}, { asType: 'agent' }) as RootTrace
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
rootSpan._sessionId = params.sessionId
logForDebugging(`[langfuse] Sub-agent trace created: ${rootSpan.id} (type=${params.agentType})`)
return rootSpan as unknown as LangfuseSpan
} catch (e) {
logForDebugging(`[langfuse] createSubagentTrace failed: ${e}`, { level: 'error' })
return null
}
}
export function endTrace(rootSpan: LangfuseSpan | null, output?: unknown): void {
if (!rootSpan) return
try {
if (output !== undefined) {
rootSpan.update({ output })
}
rootSpan.end()
logForDebugging(`[langfuse] Trace ended: ${rootSpan.id}`)
} catch (e) {
logForDebugging(`[langfuse] endTrace failed: ${e}`, { level: 'error' })
}
}