优化llm分析逻辑

This commit is contained in:
dark 2026-06-04 11:55:38 +08:00
parent d3f5c82d98
commit 9e10bf11cf
5 changed files with 109 additions and 13 deletions

View File

@ -158,15 +158,7 @@ class OpenAICompatibleLlmClient:
self.action_analysis_prompt, self.action_analysis_prompt,
{ {
"action": action, "action": action,
"result": { "result": _action_review_result_payload(action, result),
"backend": result.backend,
"ok": result.ok,
"exit_code": result.exit_code,
"tool_name": result.tool_name,
"values": _redact_sensitive(result.values),
"stderr": _truncate_text(result.stderr),
"error_summary": result.error_summary,
},
"state_summary": _redact_sensitive(state_summary), "state_summary": _redact_sensitive(state_summary),
}, },
) )
@ -325,6 +317,45 @@ def _redact_sensitive(value: Any) -> Any:
return value return value
def _action_review_result_payload(action: str, result: ActionResult) -> dict[str, Any]:
"""构造 action 审核输入,避免把正常脚本日志当作错误喂给 LLM。"""
payload: dict[str, Any] = {
"backend": result.backend,
"ok": result.ok,
"exit_code": result.exit_code,
"tool_name": result.tool_name,
"values": _redact_sensitive(result.values),
"error_summary": result.error_summary,
}
if _needs_diagnostic_log(action, result):
diagnostic = _diagnostic_log_text(result)
if diagnostic:
payload["diagnostic_log"] = diagnostic
return payload
def _needs_diagnostic_log(action: str, result: ActionResult) -> bool:
"""仅在失败或业务异常时把少量诊断日志交给 LLM。"""
if not result.ok or result.error_summary or result.values.get("PENDING_AGENT_CONFIRMATION"):
return True
if action == "verify-ip":
success = result.values.get("SUCCESS")
if success is not None and str(success).lower() not in ("true", "1", "yes"):
return True
return False
def _diagnostic_log_text(result: ActionResult) -> str:
"""优先使用错误摘要;必要时取 stderr/stdout/raw_output 的尾部作为诊断上下文。"""
if result.error_summary:
return _truncate_text(result.error_summary)
for text in (result.stderr, result.stdout, result.raw_output):
stripped = text.strip()
if stripped:
return _tail_text(stripped)
return ""
def _truncate_text(value: str, limit: int = 1000) -> str: def _truncate_text(value: str, limit: int = 1000) -> str:
"""截断发送给 LLM 的长文本,避免传入完整日志。""" """截断发送给 LLM 的长文本,避免传入完整日志。"""
if len(value) <= limit: if len(value) <= limit:
@ -332,6 +363,14 @@ def _truncate_text(value: str, limit: int = 1000) -> str:
return value[:limit] + "...[已截断]" return value[:limit] + "...[已截断]"
def _tail_text(value: str, limit: int = 1000) -> str:
"""保留长诊断日志尾部,通常错误原因更靠近末尾。"""
if len(value) <= limit:
return value
marker = "[已截断]..."
return marker + value[-(limit - len(marker)) :]
def _string(payload: dict[str, Any], key: str, default: str) -> str: def _string(payload: dict[str, Any], key: str, default: str) -> str:
"""安全读取字符串字段。""" """安全读取字符串字段。"""
value = payload.get(key, default) value = payload.get(key, default)

View File

@ -83,5 +83,7 @@ ACTION_ANALYSIS_PROMPT = """分析一次 PAM action 执行结果。
要求 要求
- 必须明确给出 `should_continue`没有问题时为 true存在需要人工判断的问题时为 false - 必须明确给出 `should_continue`没有问题时为 true存在需要人工判断的问题时为 false
- 如果 exit_code 0ok=falseverify-ip SUCCESS=false出现 pending_confirmation应标记异常 - 如果 exit_code 0ok=falseverify-ip SUCCESS=false出现 pending_confirmation应标记异常
- 主要依据结构化字段 `ok``exit_code``values``error_summary` 判断只有输入里存在 `diagnostic_log` 才把它当作异常诊断上下文
- 脚本正常过程日志不会作为错误依据不能因为日志来自 stderr 就判定异常
- 不要输出密钥tokenAuthorization 或完整日志原文 - 不要输出密钥tokenAuthorization 或完整日志原文
""" """

View File

@ -179,7 +179,6 @@ class RuleBasedLlmClient:
"exit_code": result.exit_code, "exit_code": result.exit_code,
"tool_name": result.tool_name, "tool_name": result.tool_name,
"values": result.values, "values": result.values,
"stderr": result.stderr,
"error_summary": result.error_summary, "error_summary": result.error_summary,
}, },
max_text_len=1000, max_text_len=1000,
@ -197,7 +196,7 @@ class RuleBasedLlmClient:
if not result.ok: if not result.ok:
severity = "medium" severity = "medium"
possible_reason = result.error_summary or "action 返回失败状态。" possible_reason = result.error_summary or "action 返回失败状态。"
suggested_action = "查看 action stderr/raw_output确认参数、网络和目标服务状态。" suggested_action = "查看 action 诊断日志、参数、网络和目标服务状态。"
notes.append("硬规则检测到 action 执行失败。") notes.append("硬规则检测到 action 执行失败。")
should_continue = False should_continue = False

View File

@ -15,4 +15,6 @@
要求: 要求:
- 必须明确给出 `should_continue`:没有问题时为 true存在需要人工判断的问题时为 false。 - 必须明确给出 `should_continue`:没有问题时为 true存在需要人工判断的问题时为 false。
- 如果 exit_code 非 0、ok=false、verify-ip SUCCESS=false、出现 pending_confirmation应标记异常。 - 如果 exit_code 非 0、ok=false、verify-ip SUCCESS=false、出现 pending_confirmation应标记异常。
- 主要依据结构化字段 `ok`、`exit_code`、`values`、`error_summary` 判断;只有输入里存在 `diagnostic_log` 时,才把它当作异常诊断上下文。
- 脚本正常过程日志不会作为错误依据,不能因为日志来自 stderr 就判定异常。
- 不要输出密钥、token、Authorization 或完整日志原文。 - 不要输出密钥、token、Authorization 或完整日志原文。

View File

@ -1,4 +1,5 @@
from dataclasses import asdict from dataclasses import asdict
import json
from pam_deploy_graph.agent import PamDeployAgent from pam_deploy_graph.agent import PamDeployAgent
from pam_deploy_graph.checkpoint_store import redact_mapping from pam_deploy_graph.checkpoint_store import redact_mapping
@ -219,13 +220,66 @@ def test_openai_compatible_client_analyzes_action_result_with_redaction():
ok=False, ok=False,
values={"CLIENT_SECRET": "real-secret", "SUCCESS": "false"}, values={"CLIENT_SECRET": "real-secret", "SUCCESS": "false"},
stderr="x" * 1200, stderr="x" * 1200,
error_summary="failed",
), ),
state_summary={"params": {"CLIENT_SECRET": "real-secret"}}, state_summary={"params": {"CLIENT_SECRET": "real-secret"}},
) )
serialized_prompt = str(calls[0]) serialized_prompt = str(calls[0])
input_payload = _llm_input_payload(calls[0])
assert analysis.has_anomaly is True assert analysis.has_anomaly is True
assert analysis.severity == "high" assert analysis.severity == "high"
assert "real-secret" not in serialized_prompt assert "real-secret" not in serialized_prompt
assert "[已截断]" in serialized_prompt assert input_payload["result"]["diagnostic_log"].startswith("[已截断]...")
def test_openai_compatible_client_omits_success_script_logs_from_action_review():
calls = []
def transport(url, headers, payload, timeout_sec):
calls.append(payload)
return {
"choices": [
{
"message": {
"content": (
'{"action":"get-online-ips","has_anomaly":false,"severity":"info",'
'"possible_reason":"","suggested_action":"continue",'
'"requires_confirmation":false,"should_continue":true}'
)
}
}
]
}
client = OpenAICompatibleLlmClient(
base_url="https://llm.example/v1",
api_key="secret-key",
model="model-a",
transport=transport,
)
client.analyze_action_result(
action="get-online-ips",
result=ActionResult(
action="get-online-ips",
backend="script",
ok=True,
values={"ACTION": "get-online-ips", "COUNT": "1", "IP": ["10.4.1.1"]},
stdout="ACTION=get-online-ips\nCOUNT=1\nIP=10.4.1.1\n",
stderr="[INFO] [FLOW][START] get_token\n[INFO] [FLOW][DONE] get_online_ips\n",
),
state_summary={},
)
input_payload = _llm_input_payload(calls[0])
result_payload = input_payload["result"]
assert "diagnostic_log" not in result_payload
assert "stdout" not in result_payload
assert "stderr" not in result_payload
assert "[FLOW][START]" not in json.dumps(input_payload, ensure_ascii=False)
def _llm_input_payload(request_payload):
content = request_payload["messages"][1]["content"]
_, _, raw_json = content.partition("输入 JSON:\n")
return json.loads(raw_json)