优化llm分析逻辑
This commit is contained in:
parent
d3f5c82d98
commit
9e10bf11cf
@ -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)
|
||||||
|
|||||||
@ -83,5 +83,7 @@ ACTION_ANALYSIS_PROMPT = """分析一次 PAM action 执行结果。
|
|||||||
要求:
|
要求:
|
||||||
- 必须明确给出 `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 或完整日志原文。
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 或完整日志原文。
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user