fix:Jev 观测改为 evaluator,会话视图不再被 JSON 分类结果占位 #14

Merged
jiawei merged 1 commits from feat/0.3.0 into main 2026-10-18 19:39:46 +08:00
2 changed files with 22 additions and 24 deletions
Showing only changes of commit b5cc6f9aec - Show all commits
+3 -3
View File
@@ -63,8 +63,8 @@ class DecisionEngine:
from app.observability.tracing import observe_decision, record_decision from app.observability.tracing import observe_decision, record_decision
# 观测包在请求外面:成功才记结果,失败仍抛原来的 DecisionError # 观测包在请求外面:成功才记结果,失败仍抛原来的 DecisionError
# generation 为 None 时 record_decision 什么都不做 # observation 为 None 时 record_decision 什么都不做
with observe_decision(request) as generation: with observe_decision(request) as observation:
try: try:
response = await client.system_one( response = await client.system_one(
state=request.state, state=request.state,
@@ -93,7 +93,7 @@ class DecisionEngine:
except TypeSafeError as error: except TypeSafeError as error:
raise DecisionError("Jev SDK 调用失败", kind="sdk") from error raise DecisionError("Jev SDK 调用失败", kind="sdk") from error
parsed = self._parse_response(request, response) parsed = self._parse_response(request, response)
record_decision(generation, parsed) record_decision(observation, parsed)
return parsed return parsed
@staticmethod @staticmethod
+19 -21
View File
@@ -1,6 +1,6 @@
"""一次大屏图调用的 Langfuse 追踪 """一次大屏图调用的 Langfuse 追踪
不属于处理管道的任一步。把图调用收成可过滤的 trace,并把 Jev 记成当前节点下的 generation 不属于处理管道的任一步。把图调用收成可过滤的 trace,并把 Jev 记成当前节点下的 evaluator
每轮调用单独记录,首次请求与澄清恢复通过 session 关联,不修改业务状态 每轮调用单独记录,首次请求与澄清恢复通过 session 关联,不修改业务状态
""" """
@@ -100,7 +100,7 @@ def _screen_trace(payload, thread_id, session_id, user_id=None):
# 明确回调所属项目,避免进程中存在多个客户端时跳过追踪 # 明确回调所属项目,避免进程中存在多个客户端时跳过追踪
handler = CallbackHandler(public_key=os.getenv("LANGFUSE_PUBLIC_KEY")) handler = CallbackHandler(public_key=os.getenv("LANGFUSE_PUBLIC_KEY"))
# 在当前异步上下文执行回调,手动记录的 Jev generation 才会挂到所属节点。 # 在当前异步上下文执行回调,手动记录的 Jev 观测才会挂到所属节点。
handler.run_inline = True handler.run_inline = True
resume = isinstance(payload, Command) and payload.resume is not None resume = isinstance(payload, Command) and payload.resume is not None
trace_input = ( trace_input = (
@@ -162,9 +162,10 @@ def _run_output(state: dict[str, Any]) -> dict[str, Any]:
@contextmanager @contextmanager
def observe_decision(request: Any) -> Iterator[Any]: def observe_decision(request: Any) -> Iterator[Any]:
"""在当前节点下面记一次 Jev 调用,yield 的是 generation,未追踪时是 None """在当前节点下面记一次 Jev 调用,yield 的是观测对象,未追踪时是 None
TypeSafe 不走 LangChain callback,所以要手记。类型必须是 generation,面板才能算模型和 token TypeSafe 不走 LangChain callback,所以要手记。类型不能是 generation:会话视图只用 generation
拼对话,且只显示最浅的一条对话线,Jev 比 agent 的模型调用浅,用 generation 会把真正的对话挤掉
只有图回调已经挂上当前 span 时才记。没有父 span 就跳过,避免 Jev 单独变成一条 trace 只有图回调已经挂上当前 span 时才记。没有父 span 就跳过,避免 Jev 单独变成一条 trace
这里不吞异常:失败时标 ERROR 后原样抛出,调用方的 DecisionError 类型不变 这里不吞异常:失败时标 ERROR 后原样抛出,调用方的 DecisionError 类型不变
""" """
@@ -177,7 +178,7 @@ def observe_decision(request: Any) -> Iterator[Any]:
name = "classify-intent" if set(request.decisions) == {"intent"} else "run-decision" name = "classify-intent" if set(request.decisions) == {"intent"} else "run-decision"
# 复用本应用已经选定的客户端 # 复用本应用已经选定的客户端
with client.start_as_current_observation( with client.start_as_current_observation(
as_type="generation", as_type="evaluator",
name=name, name=name,
model=model, model=model,
input={ input={
@@ -187,11 +188,11 @@ def observe_decision(request: Any) -> Iterator[Any]:
}, },
"state": request.state, "state": request.state,
}, },
) as generation: ) as observation:
try: try:
yield generation yield observation
except Exception as error: except Exception as error:
generation.update( observation.update(
level="ERROR", level="ERROR",
status_message=str(error), status_message=str(error),
metadata=_error_metadata(error), metadata=_error_metadata(error),
@@ -199,29 +200,26 @@ def observe_decision(request: Any) -> Iterator[Any]:
raise raise
def record_decision(generation: Any, response: Any) -> None: def record_decision(observation: Any, response: Any) -> None:
"""把成功的 Jev 结果写回刚才那条 generation """把成功的 Jev 结果写回刚才那条观测
generation 为 None 表示这次没追踪,直接返回,不影响判断结果 observation 为 None 表示这次没追踪,直接返回,不影响判断结果
只写选项、置信度和 token。不写原始 HTTP 响应 只写选项、置信度和 token。不写原始 HTTP 响应
""" """
if generation is None: if observation is None:
return return
usage: dict[str, int] = {} # evaluator 没有 model/usage 字段,模型和 token 只能放 metadata
metadata = {"model": response.model, "request_id": response.request_id or ""}
if response.usage.input_tokens is not None: if response.usage.input_tokens is not None:
usage["input_tokens"] = response.usage.input_tokens metadata["input_tokens"] = str(response.usage.input_tokens)
if response.usage.output_tokens is not None: if response.usage.output_tokens is not None:
usage["output_tokens"] = response.usage.output_tokens metadata["output_tokens"] = str(response.usage.output_tokens)
generation.update( observation.update(
model=response.model,
output={ output={
name: _result_summary(result) for name, result in response.results.items() name: _result_summary(result) for name, result in response.results.items()
}, },
usage_details=usage or None, metadata=metadata,
metadata={
"request_id": response.request_id or "",
},
) )