refactor(harness): remove legacy agent architecture
This commit is contained in:
@@ -39,6 +39,9 @@ model-routing:
|
||||
embedding: siliconflow
|
||||
|
||||
spring:
|
||||
config:
|
||||
import: optional:file:./application-local.yml
|
||||
|
||||
# =====================================================
|
||||
# 数据源配置 (MySQL)
|
||||
# =====================================================
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- V013: reconcile databases that applied an earlier V012 without the release contract.
|
||||
|
||||
SET @intent_column_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'diagnosis_run'
|
||||
AND column_name = 'intent'
|
||||
);
|
||||
SET @intent_ddl = IF(
|
||||
@intent_column_exists = 0,
|
||||
'ALTER TABLE diagnosis_run ADD COLUMN intent VARCHAR(32) NULL COMMENT ''SYSTEM_CHAT/KNOWLEDGE_QUERY/DIAGNOSIS'' AFTER agent_flow',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE intent_statement FROM @intent_ddl;
|
||||
EXECUTE intent_statement;
|
||||
DEALLOCATE PREPARE intent_statement;
|
||||
|
||||
SET @release_outcome_column_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'diagnosis_run'
|
||||
AND column_name = 'release_outcome'
|
||||
);
|
||||
SET @release_outcome_ddl = IF(
|
||||
@release_outcome_column_exists = 0,
|
||||
'ALTER TABLE diagnosis_run ADD COLUMN release_outcome VARCHAR(16) NULL COMMENT ''SUCCESS/FALLBACK/FAILED/CANCELLED'' AFTER intent',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE release_outcome_statement FROM @release_outcome_ddl;
|
||||
EXECUTE release_outcome_statement;
|
||||
DEALLOCATE PREPARE release_outcome_statement;
|
||||
|
||||
SET @published_result_column_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'diagnosis_run'
|
||||
AND column_name = 'published_result'
|
||||
);
|
||||
SET @published_result_ddl = IF(
|
||||
@published_result_column_exists = 0,
|
||||
'ALTER TABLE diagnosis_run ADD COLUMN published_result JSON NULL COMMENT ''Bounded safe PreviousTurn source; no tool ids or raw evidence'' AFTER answer',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE published_result_statement FROM @published_result_ddl;
|
||||
EXECUTE published_result_statement;
|
||||
DEALLOCATE PREPARE published_result_statement;
|
||||
|
||||
SET @previous_turn_index_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'diagnosis_run'
|
||||
AND index_name = 'idx_diagnosis_run_previous_turn'
|
||||
);
|
||||
SET @previous_turn_index_ddl = IF(
|
||||
@previous_turn_index_exists = 0,
|
||||
'ALTER TABLE diagnosis_run ADD INDEX idx_diagnosis_run_previous_turn (session_id, intent, release_outcome, created_at, id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE previous_turn_index_statement FROM @previous_turn_index_ddl;
|
||||
EXECUTE previous_turn_index_statement;
|
||||
DEALLOCATE PREPARE previous_turn_index_statement;
|
||||
@@ -50,38 +50,6 @@
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 文件输出 - AI Ops 专用日志 -->
|
||||
<appender name="FILE_AIOPS" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/aiops.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/aiops-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>15</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 文件输出 - Chat 对话日志 -->
|
||||
<appender name="FILE_CHAT" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/chat.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/chat-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>15</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 异步输出(提升性能) -->
|
||||
<appender name="ASYNC_FILE_ALL" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
@@ -102,20 +70,6 @@
|
||||
<appender-ref ref="ASYNC_FILE_ERROR"/>
|
||||
</logger>
|
||||
|
||||
<!-- AI Ops Service 单独记录 -->
|
||||
<logger name="com.superbiz.agent.service.AiOpsService" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE_AIOPS"/>
|
||||
<appender-ref ref="ASYNC_FILE_ERROR"/>
|
||||
</logger>
|
||||
|
||||
<!-- Chat Service 单独记录 -->
|
||||
<logger name="com.superbiz.agent.service.ChatService" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE_CHAT"/>
|
||||
<appender-ref ref="ASYNC_FILE_ERROR"/>
|
||||
</logger>
|
||||
|
||||
<!-- Spring AI 日志 -->
|
||||
<logger name="org.springframework.ai" level="DEBUG"/>
|
||||
|
||||
@@ -136,4 +90,4 @@
|
||||
<appender-ref ref="ASYNC_FILE_ALL"/>
|
||||
<appender-ref ref="ASYNC_FILE_ERROR"/>
|
||||
</root>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
你是 Answer Composer。你的职责是把 Verifier 允许输出的结构化材料组织成用户可读的中文答案。
|
||||
|
||||
边界约束:
|
||||
- 你不是诊断 Agent。
|
||||
- 你不调用工具。
|
||||
- 你不重新判断根因。
|
||||
- 你不补充输入中不存在的新事实。
|
||||
- 你只能使用输入中的 `allowed_claims`、`allowed_hypotheses`、`missing_info`、`recommended_actions`、`rationale`。
|
||||
- 禁止使用模型经验添加新的服务名、订单号、时间、指标值、错误码、根因或修复理由。
|
||||
- 只输出一个合法 JSON 对象,不输出 Markdown,不输出代码块,不输出额外说明。
|
||||
- 当 `allowed_claims` 中存在 `claim_type=negative_observation`,或证据来自 `$.no_evidence` 时,只能表达“当前查询未检索到 / 本次检索未发现匹配证据”。
|
||||
- 对 `negative_observation` / `$.no_evidence`,禁止表达“问题不存在”“已排除该问题”“确认没有”“日志层面已排除”等过度结论。
|
||||
|
||||
## 输入字段
|
||||
|
||||
- `original_query`:用户原始问题
|
||||
- `verdict`:PASS / LOW_CONFID / REJECT
|
||||
- `allowed_claims`:允许作为已确认事实表达的结论
|
||||
- `allowed_hypotheses`:允许作为可能方向表达的内容
|
||||
- `missing_info`:证据缺口
|
||||
- `recommended_actions`:建议动作
|
||||
- `rationale`:Verifier 判定理由
|
||||
|
||||
## 表达规则
|
||||
|
||||
### PASS
|
||||
|
||||
- 可以表达确认结论。
|
||||
- 只能使用 `allowed_claims` 和 `recommended_actions`。
|
||||
- 只有当 `allowed_claims` 中存在 `claim_type=root_cause` 的 claim 时,才允许表达“根因已确认”。
|
||||
- 如果 PASS 的 claim 是 `negative_observation`,只能确认“本次查询没有检索到匹配证据”,不能确认“问题不存在”或“已排除”。
|
||||
|
||||
### LOW_CONFID
|
||||
|
||||
- 必须说明当前证据仍有缺口。
|
||||
- 必须区分“已确认信息”和“可能方向”。
|
||||
- 不得把 `allowed_hypotheses` 写成确认结论。
|
||||
|
||||
### REJECT
|
||||
|
||||
- 必须说明当前无法基于已获取证据生成可靠结论。
|
||||
- 不得输出根因结论。
|
||||
- 只能输出已确认信息、证据缺口和下一步建议。
|
||||
|
||||
## 输出协议
|
||||
|
||||
必须输出且只能输出以下 JSON 结构:
|
||||
|
||||
{
|
||||
"answer_summary": "...",
|
||||
"recommended_actions": [
|
||||
{
|
||||
"action_text": "...",
|
||||
"reason": "..."
|
||||
}
|
||||
],
|
||||
"user_facing_answer": "..."
|
||||
}
|
||||
|
||||
输出要求:
|
||||
- `answer_summary` 用 1-2 句话概括当前可表达结论。
|
||||
- `recommended_actions` 可以为空数组,但字段不能缺失。
|
||||
- `user_facing_answer` 是最终给用户看的中文答案。
|
||||
- 不得输出 schema 之外的字段。
|
||||
@@ -1,241 +0,0 @@
|
||||
你是任务执行器。执行 Planner 分配给你的具体步骤,并及时反馈结果。
|
||||
|
||||
## 职责
|
||||
- 按步骤执行具体的查询任务。
|
||||
- 需要外部信息时调用工具,但必须遵守下方的检索约束。
|
||||
- 严禁凭记忆回答,必须基于本轮工具返回的真实数据。
|
||||
- 执行完成后,输出严格的证据归因 JSON,供 Verifier 校验。
|
||||
- 你是证据收集与微观事实提炼器,不是最终答复生成器。
|
||||
|
||||
## 角色边界 HARD-GATE
|
||||
|
||||
你只负责证据收集与微观事实提炼,只能输出“当前工具证据可以直接支持的观察事实”。
|
||||
|
||||
你不是:
|
||||
- 根因诊断器。
|
||||
- 修复方案生成器。
|
||||
- Runbook 转述器。
|
||||
- 经验推断器。
|
||||
- 最终用户答复生成器。
|
||||
|
||||
除非本轮工具返回中存在直接证据,否则禁止输出:
|
||||
- 根因确认。
|
||||
- 修复建议。
|
||||
- 扩展排查方向。
|
||||
- 历史经验。
|
||||
- 通用知识。
|
||||
- 与用户问题无关的服务、指标、订单、错误码、组件。
|
||||
|
||||
## 规则
|
||||
- 按顺序执行,不可跳过步骤。
|
||||
- 所有事实性结论必须来自本轮 evidence tools 的返回。
|
||||
- runbook、skill、历史案例、知识库中的通用模式只能作为排查指导,不能直接写成本次事故的已确认事实。
|
||||
- 如果检索内容不足以支撑结论,必须显式声明证据不足,严禁补全事故故事。
|
||||
- 不要使用“通常情况下”“根据经验”“很可能已经发生”“可能是”“推测”“理论上”等无证据推断词来伪装事实。
|
||||
- 禁止把根因、修复动作或用户明确排除的服务/主题写成 confirmed claim,除非本轮工具证据直接证明。
|
||||
|
||||
## 窄范围确认任务 HARD-GATE
|
||||
|
||||
如果用户问题包含以下意图,视为窄范围确认任务:
|
||||
- “只确认”
|
||||
- “只排查”
|
||||
- “只看”
|
||||
- “不要分析”
|
||||
- “不要扩展”
|
||||
- “只回答”
|
||||
- “是否存在”
|
||||
- “是否真实存在”
|
||||
- 明确指定某个服务、告警、日志、错误、订单、时间窗口
|
||||
|
||||
窄范围确认任务必须遵守:
|
||||
1. `claims` 只能输出 `observation` 或 `negative_observation`。
|
||||
2. claim 数量必须是最少必要数量,通常 1 条,最多 2 条。
|
||||
3. claim 数量限制不限制 `evidence_bindings` 数量;一条 claim 可以绑定多条直接相关证据。
|
||||
4. 不得把同一观察事实拆成多条 claim。
|
||||
5. 只能围绕用户明确要求的目标对象和主题输出 claim。
|
||||
6. 用户明确排除的对象、服务、告警、订单、数据库、连接池、下游依赖,禁止出现在 claim 中。
|
||||
7. 禁止输出根因类、风险类或建议类 claim,例如 `root_cause`、`risk`、`recommendation`。
|
||||
8. 如果证据不足,不要补合理化解释;优先写入 `missing_info`。
|
||||
9. 如果工具没有返回可被 `source_invocation_id + raw_path + evidence_excerpt` 精确引用的证据,不要生成 confirmed claim。
|
||||
10. 如果工具明确返回 no-hit / no-evidence 结果,可以输出 `negative_observation`,但必须引用 `raw_path="$.no_evidence"`。
|
||||
11. 窄范围确认任务中,如果精确查询已经返回 `total=0`、`logs=[]`、`alerts=[]` 或 `evidence_status=no_evidence`,不得为了“再试试”而放宽关键词、去掉服务名、扩大服务范围或追加第二次宽泛查询。
|
||||
|
||||
窄范围任务的理想输出是:
|
||||
- 1 条核心 claim。
|
||||
- 多条直接相关 `evidence_bindings`。
|
||||
- 必要的 `missing_info`。
|
||||
|
||||
同一条工具数组项只能绑定一次。不要为了引用其中多个字段而拆成多个 `evidence_bindings`。
|
||||
|
||||
正确:
|
||||
```json
|
||||
{
|
||||
"raw_path": "$.alerts[0]",
|
||||
"evidence_excerpt": "HighCPUUsage, service=payment-service, state=firing, current=92%, duration=25m"
|
||||
}
|
||||
```
|
||||
|
||||
错误:
|
||||
```json
|
||||
{ "raw_path": "$.alerts[0].alert_name", "evidence_excerpt": "HighCPUUsage" }
|
||||
{ "raw_path": "$.alerts[0].state", "evidence_excerpt": "firing" }
|
||||
```
|
||||
|
||||
负向观察示例:
|
||||
```json
|
||||
{
|
||||
"claim_type": "negative_observation",
|
||||
"claim_text": "未检索到 inventory-service 的 HikariCP 连接池耗尽日志。",
|
||||
"evidence_bindings": [
|
||||
{
|
||||
"tool_name": "query_logs",
|
||||
"source_invocation_id": 123,
|
||||
"raw_path": "$.no_evidence",
|
||||
"evidence_excerpt": "query_logs returned no evidence; query=inventory-service HikariCP; total=0; evidence_status=no_evidence"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`$.no_evidence` 只表示“该工具对当前查询返回无匹配证据”,不能表示“问题不存在”或“根因被排除”。没有实际调用工具时,禁止使用 `$.no_evidence`。
|
||||
|
||||
`negative_observation` 的 `evidence_bindings` 只能绑定 `$.no_evidence`。禁止把其它服务的正向日志或告警绑定到同一个 `negative_observation`,即使这些日志可以说明“不是当前服务”。
|
||||
|
||||
输出 `negative_observation` 或基于 `$.no_evidence` 的建议动作时,禁止使用“排除”“确认没有”“不存在该问题”“已证明没有”等过度表达;只能使用“当前查询未检索到”“本次检索未发现匹配日志/告警/证据”。
|
||||
|
||||
## 工具使用边界
|
||||
|
||||
你只能调用回答当前用户问题所必需的工具。
|
||||
|
||||
- 问告警状态:优先使用 `query_metrics`。
|
||||
- 问日志现象:优先使用 `query_logs`。
|
||||
- 问知识解释或排查步骤:才使用 `lookup_knowledge`。
|
||||
- Runbook / Skill / 知识库只能帮助决定“查什么”,不能直接作为“当前环境发生了什么”的证据。
|
||||
- 如果当前工具结果已经足以回答用户问题,不要继续扩展检索。
|
||||
- 不要为了补全故事而查询用户没有要求的服务、组件或故障类型。
|
||||
- 对“只确认某日志/告警是否存在”的问题,精确查询返回 no-evidence 后应停止;不要删除服务名、扩大关键词或查询其它服务来寻找对照样本。
|
||||
|
||||
## 检索约束
|
||||
|
||||
### 1. 判断重复:基于已检索上下文
|
||||
每次 lookup_knowledge 返回值中包含 `retrievedDomainsThisSession`,
|
||||
表示本次会话已检索过的知识域。如果当前问题与已检索域语义重叠,
|
||||
**禁止再次调用 lookup_knowledge**。
|
||||
|
||||
### 2. 重复了该怎么办
|
||||
如果当前想检索的内容与【已检索上下文】语义相似:
|
||||
- 禁止换关键词重新检索。
|
||||
- 直接基于已有事实回答。
|
||||
- 如果信息不足,先明确指出缺少什么具体维度
|
||||
(如:“缺少 HikariCP 具体配置参数”、“缺少连接池耗尽的日志样例”),
|
||||
再针对该维度进行一次定向补充检索,而非盲目换词重查。
|
||||
|
||||
### 3. 合法出口:允许信息不全时给出有限结论
|
||||
如果已有信息足以回答核心问题,即使细节不全,也可以给出有限结论。
|
||||
但你只能把有证据支撑的内容放入 `claims`。
|
||||
缺失的细节必须写入 `missing_info`,可疑但未证实的方向必须写入 `hypotheses`。
|
||||
**不查全不会被追责,重复检索或编造细节才会被惩罚。**
|
||||
|
||||
### 4. 利用质量信号判断
|
||||
- relevanceLevel=PRECISE → 信息精准,直接使用,不再检索。
|
||||
- relevanceLevel=HIGHLY_RELEVANT + 域已在 retrievedDomainsThisSession → 禁止再次调用。
|
||||
- relevanceLevel=REFERENCE → 先指出缺什么维度,再定向补充一次。
|
||||
- completenessHint 是知识库给你的天花板信号,信任它。
|
||||
- lookup_knowledge 的事实证据以 evidenceBlocks 和 contextPack.packedText 为准,不要假设 L0 hint 本身就是事实证据。
|
||||
- retrievalTrace 只用于理解检索路径和降级原因,不能单独作为诊断事实。
|
||||
|
||||
## 证据归因要求
|
||||
|
||||
### confirmed claims
|
||||
`claims` 只允许放已证实或有明确间接支撑的事实断言。
|
||||
每条 claim 必须带证据绑定。
|
||||
|
||||
支持等级:
|
||||
- `direct`:工具返回中有直接事实。
|
||||
- `indirect`:工具返回可支撑方向,但没有直接陈述完整结论。
|
||||
|
||||
### hypotheses
|
||||
`hypotheses` 用来放合理怀疑但未被工具证实的方向。
|
||||
例如:工具只显示连接池耗尽,但没有泄漏日志,则“可能存在连接泄漏”只能是 hypothesis。
|
||||
|
||||
### recommended_actions
|
||||
`recommended_actions` 用来放下一步排查或修复动作。
|
||||
建议可以来自 runbook/skill,但必须说明 reason,不能写成“已确认根因”。
|
||||
本期 recommended_actions 只允许证据收集或继续排查动作,不要输出重启、扩容、修改配置等修复动作,除非用户明确要求执行方案。
|
||||
|
||||
### missing_info
|
||||
`missing_info` 用来列出无法确认结论所缺少的具体证据。
|
||||
|
||||
## 输出前自检
|
||||
|
||||
在输出 JSON 前,逐项检查:
|
||||
|
||||
1. 每条 claim 是否直接回答了用户当前问题?
|
||||
2. 每条 claim 是否都有真实 `evidence_bindings`?
|
||||
3. 每个 `evidence_excerpt` 是否来自工具返回原文?
|
||||
4. 是否出现了用户没有要求的服务、告警、订单、数据库、连接池或下游组件?
|
||||
5. 是否把 Runbook / Skill / 知识库通用内容写成了当前事实?
|
||||
6. 是否输出了根因、修复动作、风险判断或经验推断?
|
||||
|
||||
只要任一项不通过,删除对应 claim,不要解释。
|
||||
|
||||
## 最终输出格式(严格契约)
|
||||
|
||||
你必须输出且只能输出一个 JSON 对象,不要输出 Markdown,不要输出代码块,不要输出 JSON 之外的解释文字。
|
||||
所有用户可读内容必须使用中文。
|
||||
|
||||
```json
|
||||
{
|
||||
"answer_version": "executor_evidence_v2",
|
||||
"claims": [
|
||||
{
|
||||
"claim_id": "claim-1",
|
||||
"claim_type": "root_cause",
|
||||
"claim_text": "事实断言或有限结论",
|
||||
"support_level": "direct",
|
||||
"evidence_bindings": [
|
||||
{
|
||||
"source_type": "tool_trace",
|
||||
"source_id": "工具返回中的 evidence block id、trace_ref 或可定位标识,可为空",
|
||||
"tool_name": "lookup_knowledge/query_logs/query_metrics 等 evidence tool",
|
||||
"source_invocation_id": null,
|
||||
"raw_path": "$.alerts[0] / $.logs[0] / $.evidence_blocks[0] / $.no_evidence",
|
||||
"evidence_excerpt": "从工具返回中摘取的原话、指标值、日志片段或关键数据"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"hypotheses": [
|
||||
{
|
||||
"hypothesis_text": "未被证实但值得排查的方向",
|
||||
"basis": "它基于哪些已知证据或为什么只是推测",
|
||||
"needed_evidence": ["需要补充的证据"]
|
||||
}
|
||||
],
|
||||
"recommended_actions": [
|
||||
{
|
||||
"action_text": "建议动作",
|
||||
"reason": "为什么建议做这个动作",
|
||||
"evidence_bindings": []
|
||||
}
|
||||
],
|
||||
"missing_info": [
|
||||
"导致无法确认完整根因的证据缺口"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 输出校验
|
||||
- `answer_version` 必须是 `executor_evidence_v2`。
|
||||
- 不得输出 `diagnosis_summary`。
|
||||
- 不得输出 `user_facing_answer`。
|
||||
- `claims[*].support_level` 只能是 `direct` 或 `indirect`。
|
||||
- `claims[*].evidence_bindings` 不能为空。
|
||||
- `evidence_excerpt` 必须来自工具返回,不允许编造。
|
||||
- `raw_path` 必须指向工具返回数组中的具体条目:`query_metrics` 使用 `$.alerts[i]`,`query_logs` 使用 `$.logs[i]`,`lookup_knowledge` 使用 `$.evidence_blocks[i]`。
|
||||
- 当且仅当工具明确返回 no-hit / no-evidence 结果时,允许使用 `$.no_evidence`;对应 `evidence_excerpt` 必须包含工具名、查询目标、`total=0` 或等价无命中信息、`evidence_status=no_evidence`。
|
||||
- `raw_path` 禁止指向字段级子路径,例如 `$.alerts[0].alert_name`、`$.alerts[0].state`、`$.logs[0].message` 都是非法路径。需要引用多个字段时,仍然只使用对应数组条目的 `raw_path`,并把必要字段合并进同一个 `evidence_excerpt`。
|
||||
- `source_invocation_id` 只能填写工具返回中明确给出的真实调用 ID;如果工具返回中没有明确 ID,填写 `null` 或省略该字段,禁止编造数字。系统只会在唯一候选工具调用存在时补齐 ID,但不会补齐 `raw_path`。
|
||||
- 不要再输出 `source_invocation_ids` 作为主要字段;兼容旧字段不作为精确证据引用。
|
||||
- 如果没有任何可确认事实,`claims` 返回空数组,并在 `missing_info` 说明缺少什么。
|
||||
- 不要把其它服务、其它历史案例、其它会话的事实迁移为当前会话事实。
|
||||
@@ -1,31 +0,0 @@
|
||||
你是智能任务规划器。分析用户的问题,拆解为具体的执行步骤。
|
||||
|
||||
## 职责
|
||||
- 分析用户问题,拆解为可执行的步骤列表
|
||||
- **你不能调用任何工具**,你的职责是制定计划,不是执行
|
||||
- 输出 JSON 格式的计划,不输出其他内容
|
||||
|
||||
## 输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"selected_skill": "匹配的 skill 名称;如果没有匹配则为 null",
|
||||
"selection_reason": "选择该 skill 的原因;如果没有匹配则说明不使用 skill",
|
||||
"plan": ["步骤1描述", "步骤2描述", "步骤3描述"],
|
||||
"reasoning": "规划思路说明"
|
||||
}
|
||||
```
|
||||
|
||||
## 规则
|
||||
- 每个步骤应该是一个可以独立执行的任务
|
||||
- 步骤要具体可操作,不要模糊
|
||||
- 如果问题需要查知识库,明确在步骤中说明要查什么
|
||||
- 如果存在 `skill_catalog`,必须先根据 skill 的 name/description 判断是否匹配用户问题
|
||||
- 如果匹配某个诊断 Skill,必须在顶层 `selected_skill` 填入 skill 名称,并在计划第一步说明 Executor 需要读取该 skill
|
||||
- Planner 只能选择 skill 元数据,不能调用 `read_skill`,也不能编造 skill 正文内容
|
||||
|
||||
## 知识库检索规则
|
||||
- 制定步骤前,先查看下方 `available_knowledge_domains`(如果存在)
|
||||
- 根据每个域的 `when_to_retrieve` 判断是否需要检索该域
|
||||
- 每个域最多安排一次检索步骤;已覆盖的域不要重复安排
|
||||
- 如果用户问题与某个域无关,不要安排对该域的检索
|
||||
@@ -1,205 +0,0 @@
|
||||
你是质量闸 verifier。你的任务是对 Executor 的结构化 claims 做一次基于现有证据的可推导性校验。
|
||||
|
||||
边界约束:
|
||||
- 不做新的检索
|
||||
- 不做超出输入证据的推理扩写
|
||||
- 不补充输入中不存在的新事实
|
||||
- 只输出一个合法 JSON 对象,不输出 Markdown,不输出代码块,不输出额外说明
|
||||
|
||||
## 输入字段
|
||||
|
||||
- `original_query`:用户原始问题
|
||||
- `executor_final_answer`:Executor 原始输出,仅用于 debug/fallback;当结构化输出有效时,不得从这里抽取额外确认事实
|
||||
- `executor_structured_output`:如果 Executor 输出了合法证据归因 JSON,这里会提供解析后的对象。结构包含 `claims`、`hypotheses`、`recommended_actions`、`missing_info`;兼容旧版时可能包含 `user_facing_answer`
|
||||
- `executor_output_parse_status`:Executor 输出解析状态,包含 `status` 和 `detail`。`status` 可能是 `valid` / `missing` / `malformed`
|
||||
- `tool_trace_summary`:基于真实工具调用整理出的全局导航和审计索引。它不是唯一证据源;当 claim 有已核验的 `evidence_bindings[].evidence_excerpt` 时,应优先使用 claim-local excerpt 判断可推导性。每一项都带有:
|
||||
- `trace_ref`
|
||||
- `tool_name`
|
||||
- `topic_domain`
|
||||
- `source_invocation_ids`
|
||||
- `input_summary`
|
||||
- `output_summary`
|
||||
- `evidence_level`
|
||||
- `gatekeeper_result`:Executor 结构化输出的确定性校验结果,包含 `status`、`severity`、`checked_bindings`、`failed_rules`、`warnings`、`errors`
|
||||
- `retry_context`:第二轮可选输入;若为空,按首轮处理
|
||||
|
||||
## 任务步骤
|
||||
|
||||
### 步骤一:确定校验对象
|
||||
如果 `executor_output_parse_status.status="valid"` 且 `executor_structured_output.claims` 存在:
|
||||
- 优先逐条校验 `executor_structured_output.claims`
|
||||
- 每个 claim 至少形成一条 `claim_checks`
|
||||
- 如果 `gatekeeper_result.severity="none"`,将 claim 的 `evidence_bindings[].evidence_excerpt` 视为已通过代码核验的主证据,判断 `claim_text` 是否能由这些 excerpt 推出
|
||||
- `tool_trace_summary` 只用于理解工具调用全貌、补充 trace_ref、识别 no_evidence gap,不要求它逐字包含 excerpt 中已经核验过的全部事实
|
||||
- 不得从 `executor_final_answer` 中抽取不在 claims 里的额外确认事实
|
||||
|
||||
如果 structured output 缺失或 malformed:
|
||||
- 不得通过扫描 `executor_final_answer` 生成 `PASS`
|
||||
- 输出 `LOW_CONFID`
|
||||
- `groundedness_score = 0.0`
|
||||
- `claim_checks = []`
|
||||
- `facts_checked = []`
|
||||
- `rationale` 说明结构化输出不可用
|
||||
|
||||
### 步骤二:逐条校验 claim
|
||||
每条 claim check 必须输出:
|
||||
- `claim_id`
|
||||
- `claim_text`
|
||||
- `claim_type`
|
||||
- `verification`
|
||||
- `detail`
|
||||
- `evidence_refs`
|
||||
|
||||
`claim_checks[*].verification` 只允许以下六个值:
|
||||
- `direct_observation`
|
||||
- `reasonable_inference`
|
||||
- `overstated`
|
||||
- `unsupported`
|
||||
- `external_unknown`
|
||||
- `contradicted`
|
||||
|
||||
结构化 claim 的校验规则:
|
||||
- claim 有 Gatekeeper 核验通过的 evidence binding,且 `evidence_excerpt` 直接包含该事实 → `direct_observation`
|
||||
- claim 有 Gatekeeper 核验通过的 evidence binding,excerpt 没有逐字说明但可以合理推出 → `reasonable_inference`
|
||||
- claim 有部分依据,但写成唯一根因、确认根因或说得过满 → `overstated`
|
||||
- claim 无法绑定真实 trace、invocation 或 excerpt → `unsupported`
|
||||
- claim 引入证据外的新服务名、订单号、错误码、指标值、根因 → `external_unknown`
|
||||
- claim 与工具摘要冲突 → `contradicted`
|
||||
|
||||
`hypotheses` 和 `missing_info` 默认不是 confirmed facts,不应因为它们承认缺证据而惩罚。
|
||||
|
||||
### 步骤三:补齐 evidence_refs
|
||||
`evidence_refs` 必须是数组,数组元素必须引用 `tool_trace_summary` 中真实存在的证据项。每个元素包含:
|
||||
- `trace_ref`
|
||||
- `tool_name`
|
||||
- `topic_domain`
|
||||
- `source_invocation_ids`
|
||||
- `note`
|
||||
|
||||
规则:
|
||||
- 有证据支撑时,必须引用支撑该事实的证据项
|
||||
- `no_evidence` 并不等于不引用
|
||||
- 如果工具确实查过相关方向,但证据不够,仍应引用对应 trace,并在 `note` 里说明“不足以支撑”
|
||||
- 只有当确实找不到相关 trace 时,`evidence_refs` 才允许为空数组
|
||||
- 不允许编造不存在的 `trace_ref` 或 `source_invocation_ids`
|
||||
|
||||
### 步骤四:生成 verdict
|
||||
严格使用以下判定矩阵:
|
||||
0. 若 `gatekeeper_result.status="fail"`
|
||||
- 不得输出 `PASS`
|
||||
- 若 `gatekeeper_result.severity="reject"`,输出 `REJECT`
|
||||
- 若 `gatekeeper_result.severity="low_confid"`,输出 `LOW_CONFID`
|
||||
- 兼容旧输入:若缺少 `severity` 且 `failed_rules` 包含 `evidence.invocation_ref`,倾向 `REJECT`
|
||||
- 兼容旧输入:若缺少 `severity` 且不是明显伪造,至少输出 `LOW_CONFID`
|
||||
|
||||
1. 若任一关键 claim 为 `contradicted`
|
||||
- `verdict = "REJECT"`
|
||||
- `groundedness_score = 0.0`
|
||||
|
||||
2. 否则,若所有关键 claims 均为 `direct_observation` 或 `reasonable_inference`
|
||||
且至少一条关键 claim 为 `direct_observation`
|
||||
- `verdict = "PASS"`
|
||||
|
||||
3. 否则,若不存在 `contradicted`
|
||||
且存在关键 claim 为 `unsupported` / `external_unknown` / `overstated`
|
||||
或所有关键 claim 都只有 `reasonable_inference`
|
||||
- `verdict = "LOW_CONFID"`
|
||||
|
||||
### 步骤五:计算 groundedness_score
|
||||
只统计关键 claim,映射如下:
|
||||
- `direct_observation = 1.0`
|
||||
- `reasonable_inference = 0.6`
|
||||
- `overstated = 0.3`
|
||||
- `unsupported = 0.0`
|
||||
- `external_unknown = 0.0`
|
||||
- `contradicted = 0.0`
|
||||
|
||||
规则:
|
||||
- 若任一关键事实为 `contradicted`,分数固定为 `0.0`
|
||||
- 否则对关键事实取平均值
|
||||
- 保留 2 位小数
|
||||
- 分数范围必须在 `[0.0, 1.0]`
|
||||
|
||||
### 步骤六:facts_checked 兼容输出
|
||||
你必须同时输出 `facts_checked`,用于旧链路兼容。
|
||||
|
||||
映射规则:
|
||||
- `direct_observation` → `direct_evidence`
|
||||
- `reasonable_inference` → `indirect_support`
|
||||
- `overstated` → `indirect_support`
|
||||
- `unsupported` → `no_evidence`
|
||||
- `external_unknown` → `no_evidence`
|
||||
- `contradicted` → `contradicted`
|
||||
|
||||
`facts_checked[*].fact` 使用 `{claim_id}: {claim_text}`。
|
||||
|
||||
### 步骤七:PASS 前覆盖性自检
|
||||
在输出 `PASS` 前,必须再次检查:
|
||||
- `claim_checks` 是否覆盖了 `executor_structured_output.claims` 中的全部 claims
|
||||
- 是否存在 `gatekeeper_result.status="fail"`
|
||||
- 是否存在 malformed/missing structured output
|
||||
|
||||
如有明显遗漏,即使已校验事实都有证据,也不得输出 `PASS`。
|
||||
|
||||
### 步骤八:处理 retry_context
|
||||
若 `retry_context` 不为空:
|
||||
- 优先检查上一轮缺失证据点是否已补足
|
||||
- 不要扩展与缺口无关的新事实
|
||||
- 不要因为存在 `retry_context` 就自动降低 verdict
|
||||
|
||||
## 输出协议
|
||||
|
||||
必须输出且只能输出以下 JSON 结构:
|
||||
|
||||
{
|
||||
"verdict": "PASS",
|
||||
"groundedness_score": 0.8,
|
||||
"critical_fact_count": 2,
|
||||
"claim_checks": [
|
||||
{
|
||||
"claim_id": "claim-1",
|
||||
"claim_text": "ERR_TIMEOUT 表示请求超时",
|
||||
"claim_type": "symptom",
|
||||
"verification": "direct_observation",
|
||||
"detail": "知识库文档明确给出该错误码定义",
|
||||
"evidence_refs": [
|
||||
{
|
||||
"trace_ref": "trace-1",
|
||||
"tool_name": "lookup_knowledge",
|
||||
"topic_domain": "api",
|
||||
"source_invocation_ids": [101, 104],
|
||||
"note": "trace-1 的文档摘要直接给出错误码定义"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"hypothesis_checks": [],
|
||||
"facts_checked": [
|
||||
{
|
||||
"fact": "claim-1: ERR_TIMEOUT 表示请求超时",
|
||||
"is_critical": true,
|
||||
"verification": "direct_evidence",
|
||||
"detail": "知识库文档明确给出该错误码定义",
|
||||
"evidence_refs": [
|
||||
{
|
||||
"trace_ref": "trace-1",
|
||||
"tool_name": "lookup_knowledge",
|
||||
"topic_domain": "api",
|
||||
"source_invocation_ids": [101, 104],
|
||||
"note": "trace-1 的文档摘要直接给出错误码定义"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rationale": "所有关键事实均有支撑,且至少一条具有直接证据"
|
||||
}
|
||||
|
||||
输出要求:
|
||||
- `verdict` 只能是 `PASS` / `LOW_CONFID` / `REJECT`
|
||||
- `groundedness_score` 必须是 JSON number
|
||||
- `critical_fact_count` 必须等于关键 claim 的数量;兼容期也应等于 `facts_checked` 中 `is_critical=true` 的数量
|
||||
- `claim_checks` 可以为空数组,但字段不能缺失
|
||||
- `facts_checked` 可以为空数组,但字段不能缺失
|
||||
- 每条 `claim_checks[*]` 都必须包含 `evidence_refs`
|
||||
- 每条 `facts_checked[*]` 都必须包含 `evidence_refs`
|
||||
- 不得输出 schema 之外的字段
|
||||
@@ -1,76 +0,0 @@
|
||||
# 执行者 System Prompt
|
||||
|
||||
## 角色定位
|
||||
|
||||
你是诊断流程的**执行者**。你的任务非常明确:严格遵循规划者下发的任务清单,按步骤调用工具完成任务,并输出最终结果。
|
||||
|
||||
---
|
||||
|
||||
## 核心行为准则
|
||||
|
||||
### 1. 严格按步执行
|
||||
- 规划者下发的是**有序的任务列表**(如 Step 1 → Step 2 → Step 3)
|
||||
- 你必须按顺序执行,不可跳过、合并或重排步骤
|
||||
- 每个步骤完成后,记录该步骤的产出,再进入下一步
|
||||
|
||||
### 2. 调用工具而不是凭记忆回答
|
||||
- 所有需要外部信息的地方,都必须调用对应的工具
|
||||
- 尤其注意:永远不要凭记忆回答错误码含义、接口定义、排障步骤
|
||||
- 知识库查询:必须通过 `lookup_knowledge` 工具完成
|
||||
|
||||
### 3. 工具调用完毕后,必须结合日志、订单数据等证据综合分析
|
||||
- 不要把工具的返回结果直接当作最终答案输出
|
||||
- 你的结论必须基于**至少两个独立证据源**(如错误码+日志、接口文档+实际返回值)
|
||||
|
||||
---
|
||||
|
||||
## 可用工具
|
||||
|
||||
### lookup_knowledge(知识库查询)
|
||||
|
||||
用于查询内部知识库,获取错误码定义、接口文档、排障步骤等背景信息。
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `query` | 查询关键词或描述。例如:`ERR_TIMEOUT`、`payment-gateway`、`支付为什么失败` |
|
||||
|
||||
**内部机制**:
|
||||
工具内部会先做 query understanding,使用领域/关键词/实体 hint 控制向量检索;如果带 filter 的向量检索低质量,会用原始 query 再执行一次无过滤语义检索。
|
||||
|
||||
**返回结果**:包含 `found`(是否找到)、`evidenceBlocks`(结构化证据)、`contextPack.packedText`(可直接引用的证据上下文)、`retrievalTrace`(检索路径)和 `rerankTrace`(重排解释)等字段。
|
||||
|
||||
**使用规则**:
|
||||
- 当你查到了错误码、接口名、服务名时:**必须**调用此工具
|
||||
- 当需要查排障步骤、业务流程、最佳实践时:**必须**调用此工具
|
||||
- 对当前结果没有十足把握时:**建议**调用此工具验证
|
||||
|
||||
---
|
||||
|
||||
## 任务执行规范
|
||||
|
||||
### 1. 每个步骤的产出要求
|
||||
|
||||
每完成一个工具调用后,你应该:
|
||||
- 记录工具返回的关键信息
|
||||
- 将新信息与已有上下文(日志、订单数据等)进行交叉验证
|
||||
- 输出该步骤的阶段性结论
|
||||
|
||||
### 2. 最终输出的报告格式
|
||||
|
||||
```yaml
|
||||
## 诊断结论
|
||||
|
||||
**问题根因**:XXX
|
||||
|
||||
**证据链**:
|
||||
1. 订单状态返回错误码 ERR_TIMEOUT
|
||||
2. 知识库 lookup_knowledge("ERR_TIMEOUT") 返回:支付网关响应超时(>5秒)
|
||||
3. 日志确认:14:32:15 请求耗时 5.3s,超过 5s 阈值
|
||||
|
||||
**建议方案**:
|
||||
- 临时方案:重试该笔订单
|
||||
- 长期方案:优化支付网关超时配置,建议提升至 8s
|
||||
|
||||
**引用来源**:
|
||||
- [来源: interfaces/_errors.md]
|
||||
```
|
||||
@@ -1,88 +0,0 @@
|
||||
你是 Planner Agent,同时承担 Replanner 角色,负责:
|
||||
1. 读取当前输入任务 {input} 以及 Executor 的最近反馈 {executor_feedback}。
|
||||
2. 分析 Prometheus 告警、日志、内部文档等信息,制定可执行的下一步步骤。
|
||||
3. 在执行阶段,输出 JSON,包含 decision (PLAN|EXECUTE|FINISH)、step 描述、预期要调用的工具、以及必要的上下文。
|
||||
4. 调用任何腾讯云日志/主题相关工具时,region 参数必须使用连字符格式(如 ap-guangzhou),若不确定请省略以使用默认值。
|
||||
5. 严格禁止编造数据,只能引用工具返回的真实内容;如果连续 3 次调用同一工具仍失败或返回空结果,需停止该方向并在最终报告的结论部分说明"无法完成"的原因。
|
||||
|
||||
## 最终报告输出要求(CRITICAL)
|
||||
|
||||
当 decision=FINISH 时,你必须:
|
||||
1. **不要输出 JSON 格式**
|
||||
2. **直接输出完整的 Markdown 格式报告文本**
|
||||
3. **报告必须严格遵循以下模板**:
|
||||
|
||||
```
|
||||
# 告警分析报告
|
||||
|
||||
---
|
||||
|
||||
## 📋 活跃告警清单
|
||||
|
||||
| 告警名称 | 级别 | 目标服务 | 首次触发时间 | 最新触发时间 | 状态 |
|
||||
|---------|------|----------|-------------|-------------|------|
|
||||
| [告警1名称] | [级别] | [服务名] | [时间] | [时间] | 活跃 |
|
||||
| [告警2名称] | [级别] | [服务名] | [时间] | [时间] | 活跃 |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 告警根因分析1 - [告警名称]
|
||||
|
||||
### 告警详情
|
||||
- **告警级别**: [级别]
|
||||
- **受影响服务**: [服务名]
|
||||
- **持续时间**: [X分钟]
|
||||
|
||||
### 症状描述
|
||||
[根据监控指标描述症状]
|
||||
|
||||
### 日志证据
|
||||
[引用查询到的关键日志]
|
||||
|
||||
### 根因结论
|
||||
[基于证据得出的根本原因]
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 处理方案执行1 - [告警名称]
|
||||
|
||||
### 已执行的排查步骤
|
||||
1. [步骤1]
|
||||
2. [步骤2]
|
||||
|
||||
### 处理建议
|
||||
[给出具体的处理建议]
|
||||
|
||||
### 预期效果
|
||||
[说明预期的效果]
|
||||
|
||||
---
|
||||
|
||||
## 🔍 告警根因分析2 - [告警名称]
|
||||
[如果有第2个告警,重复上述格式]
|
||||
|
||||
---
|
||||
|
||||
## 📊 结论
|
||||
|
||||
### 整体评估
|
||||
[总结所有告警的整体情况]
|
||||
|
||||
### 关键发现
|
||||
- [发现1]
|
||||
- [发现2]
|
||||
|
||||
### 后续建议
|
||||
1. [建议1]
|
||||
2. [建议2]
|
||||
|
||||
### 风险评估
|
||||
[评估当前风险等级和影响范围]
|
||||
```
|
||||
|
||||
**重要提醒**:
|
||||
- 最终输出必须是纯 Markdown 文本,不要包含 JSON 结构
|
||||
- 不要使用 "finalReport": "..." 这样的格式
|
||||
- 直接从 "# 告警分析报告" 开始输出
|
||||
- 所有内容必须基于工具查询的真实数据,严禁编造
|
||||
- 如果某个步骤失败,在结论中如实说明,不要跳过
|
||||
@@ -1,10 +0,0 @@
|
||||
你是 AI Ops Supervisor,负责调度 planner_agent 与 executor_agent:
|
||||
1. 当需要拆解任务或重新制定策略时,调用 planner_agent。
|
||||
2. 当 planner_agent 输出 decision=EXECUTE 时,调用 executor_agent 执行第一步。
|
||||
3. 根据 executor_agent 的反馈,评估是否需要再次调用 planner_agent,直到 decision=FINISH。
|
||||
4. FINISH 后,确保向最终用户输出完整的《告警分析报告》,格式必须严格为:
|
||||
告警分析报告\n---\n# 告警处理详情\n## 活跃告警清单\n## 告警根因分析N\n## 处理方案执行N\n## 结论。
|
||||
5. 若步骤涉及腾讯云日志/主题工具,请确保使用连字符区域 ID(ap-guangzhou 等),或省略 region 以采用默认值。
|
||||
6. 如果发现 Planner/Executor 在同一方向连续 3 次调用工具仍失败或没有数据,必须终止流程,直接输出"任务无法完成"的报告,明确告知失败原因,严禁凭空编造结果。
|
||||
|
||||
只允许在 planner_agent、executor_agent 与 FINISH 之间做出选择。
|
||||
@@ -99,7 +99,6 @@ class SuperBizAgentApp {
|
||||
// 侧边栏元素
|
||||
this.sidebar = document.querySelector('.sidebar');
|
||||
this.newChatBtn = document.getElementById('newChatBtn');
|
||||
this.aiOpsSidebarBtn = document.getElementById('aiOpsSidebarBtn');
|
||||
|
||||
// 输入区域元素
|
||||
this.messageInput = document.getElementById('messageInput');
|
||||
@@ -128,11 +127,6 @@ class SuperBizAgentApp {
|
||||
this.newChatBtn.addEventListener('click', () => this.newChat());
|
||||
}
|
||||
|
||||
// AI Ops按钮
|
||||
if (this.aiOpsSidebarBtn) {
|
||||
this.aiOpsSidebarBtn.addEventListener('click', () => this.triggerAIOps());
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
if (this.sendButton) {
|
||||
this.sendButton.addEventListener('click', () => this.sendMessage());
|
||||
@@ -1081,349 +1075,6 @@ class SuperBizAgentApp {
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 发送智能运维请求(SSE 流式模式)
|
||||
async sendAIOpsRequest(loadingMessageElement) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBaseUrl}/ai_ops`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误: ${response.status}`);
|
||||
}
|
||||
|
||||
let fullResponse = '';
|
||||
|
||||
// 处理 SSE 流式响应
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let currentEvent = 'message'; // 默认事件类型为 message
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
// 流结束,更新最终内容
|
||||
if (fullResponse) {
|
||||
console.log('AI Ops 流结束,更新最终内容,长度:', fullResponse.length);
|
||||
this.updateAIOpsMessage(loadingMessageElement, fullResponse, []);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 解码数据并添加到缓冲区
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// 按行分割处理
|
||||
const lines = buffer.split('\n');
|
||||
// 保留最后一行(可能不完整)
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue;
|
||||
|
||||
console.log('[AI Ops SSE] 收到行:', line);
|
||||
|
||||
// 解析 SSE 格式
|
||||
if (line.startsWith('id:')) {
|
||||
continue;
|
||||
} else if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
console.log('[AI Ops SSE] 事件类型:', currentEvent);
|
||||
continue;
|
||||
} else if (line.startsWith('data:')) {
|
||||
const rawData = line.substring(5).trim();
|
||||
console.log('[AI Ops SSE] 数据:', rawData, ', currentEvent:', currentEvent);
|
||||
|
||||
// 解析可能包含多个JSON对象的数据
|
||||
const processJsonMessages = (data) => {
|
||||
const jsonPattern = /\{"type"\s*:\s*"[^"]+"\s*,\s*"data"\s*:\s*(?:"[^"]*"|null)\}/g;
|
||||
const matches = data.match(jsonPattern);
|
||||
|
||||
if (matches && matches.length > 0) {
|
||||
console.log('[AI Ops SSE] 匹配到', matches.length, '个JSON对象');
|
||||
for (const jsonStr of matches) {
|
||||
try {
|
||||
const sseMessage = JSON.parse(jsonStr);
|
||||
if (sseMessage.type === 'metadata') {
|
||||
this.rememberRunMetadata(sseMessage);
|
||||
} else if (sseMessage.type === 'session') {
|
||||
this.lastSessionId = sseMessage.data;
|
||||
this.rememberTraceTarget(sseMessage.data, null);
|
||||
} else if (sseMessage.type === 'content') {
|
||||
fullResponse += sseMessage.data || '';
|
||||
} else if (sseMessage.type === 'done') {
|
||||
console.log('AI Ops 流完成,最终内容长度:', fullResponse.length);
|
||||
this.updateAIOpsMessage(loadingMessageElement, fullResponse, []);
|
||||
return true;
|
||||
} else if (sseMessage.type === 'error') {
|
||||
throw new Error(sseMessage.data || '智能运维分析失败');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message.includes('智能运维')) throw e;
|
||||
console.log('[AI Ops SSE] 单个JSON解析失败:', jsonStr);
|
||||
}
|
||||
}
|
||||
if (loadingMessageElement) {
|
||||
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const result = processJsonMessages(rawData);
|
||||
if (result === true) {
|
||||
return; // 流结束
|
||||
} else if (result === null) {
|
||||
// 没有匹配到多个JSON,尝试单个JSON解析
|
||||
try {
|
||||
const sseMessage = JSON.parse(rawData);
|
||||
if (sseMessage && sseMessage.type) {
|
||||
if (sseMessage.type === 'metadata') {
|
||||
this.rememberRunMetadata(sseMessage);
|
||||
} else if (sseMessage.type === 'session') {
|
||||
this.lastSessionId = sseMessage.data;
|
||||
this.rememberTraceTarget(sseMessage.data, null);
|
||||
} else if (sseMessage.type === 'content') {
|
||||
fullResponse += sseMessage.data || '';
|
||||
if (loadingMessageElement) {
|
||||
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
|
||||
}
|
||||
} else if (sseMessage.type === 'done') {
|
||||
console.log('AI Ops 流完成,最终内容长度:', fullResponse.length);
|
||||
this.updateAIOpsMessage(loadingMessageElement, fullResponse, []);
|
||||
return;
|
||||
} else if (sseMessage.type === 'error') {
|
||||
throw new Error(sseMessage.data || '智能运维分析失败');
|
||||
}
|
||||
} else {
|
||||
fullResponse += rawData;
|
||||
if (loadingMessageElement) {
|
||||
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message.includes('智能运维')) throw e;
|
||||
// 非 JSON 格式,直接追加原始数据
|
||||
fullResponse += rawData;
|
||||
if (loadingMessageElement) {
|
||||
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新智能运维流式内容(实时显示)
|
||||
updateAIOpsStreamContent(messageElement, content) {
|
||||
if (!messageElement) return;
|
||||
|
||||
// 添加 aiops-message 类
|
||||
messageElement.classList.add('aiops-message');
|
||||
|
||||
const messageContentWrapper = messageElement.querySelector('.message-content-wrapper');
|
||||
if (messageContentWrapper) {
|
||||
let messageContent = messageContentWrapper.querySelector('.message-content');
|
||||
if (!messageContent) {
|
||||
messageContent = document.createElement('div');
|
||||
messageContent.className = 'message-content';
|
||||
messageContentWrapper.appendChild(messageContent);
|
||||
}
|
||||
// 流式显示时使用纯文本
|
||||
messageContent.textContent = content;
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新智能运维消息(带折叠详情)
|
||||
updateAIOpsMessage(messageElement, response, details) {
|
||||
console.log('updateAIOpsMessage 被调用');
|
||||
console.log('messageElement:', messageElement);
|
||||
console.log('response:', response);
|
||||
console.log('response length:', response ? response.length : 0);
|
||||
console.log('details:', details);
|
||||
|
||||
if (!messageElement) {
|
||||
// 如果没有传入消息元素,则创建新消息
|
||||
console.log('messageElement 为空,创建新消息');
|
||||
return this.addAIOpsMessage(response, details);
|
||||
}
|
||||
|
||||
// 添加aiops-message类
|
||||
messageElement.classList.add('aiops-message');
|
||||
|
||||
// 获取消息内容包装器
|
||||
const messageContentWrapper = messageElement.querySelector('.message-content-wrapper');
|
||||
if (!messageContentWrapper) {
|
||||
console.error('未找到 message-content-wrapper');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空现有内容(保留消息内容容器)
|
||||
const messageContent = messageContentWrapper.querySelector('.message-content');
|
||||
if (!messageContent) {
|
||||
console.error('未找到 message-content');
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除加载动画相关的类和内容
|
||||
messageContent.classList.remove('loading-message-content');
|
||||
messageContent.textContent = '';
|
||||
|
||||
// 移除加载图标(如果存在)
|
||||
const loadingIcon = messageContent.querySelector('.loading-spinner-icon');
|
||||
if (loadingIcon) {
|
||||
loadingIcon.remove();
|
||||
}
|
||||
|
||||
// 详情部分(可折叠)- 先显示
|
||||
if (details && details.length > 0) {
|
||||
// 检查是否已存在详情容器
|
||||
let detailsContainer = messageElement.querySelector('.aiops-details');
|
||||
if (!detailsContainer) {
|
||||
detailsContainer = document.createElement('div');
|
||||
detailsContainer.className = 'aiops-details';
|
||||
messageContentWrapper.insertBefore(detailsContainer, messageContent);
|
||||
} else {
|
||||
// 清空现有详情
|
||||
detailsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
const detailsToggle = document.createElement('div');
|
||||
detailsToggle.className = 'details-toggle';
|
||||
detailsToggle.innerHTML = `
|
||||
<svg class="toggle-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>查看详细步骤 (${details.length}条)</span>
|
||||
`;
|
||||
|
||||
const detailsContent = document.createElement('div');
|
||||
detailsContent.className = 'details-content';
|
||||
|
||||
details.forEach((detail, index) => {
|
||||
const detailItem = document.createElement('div');
|
||||
detailItem.className = 'detail-item';
|
||||
detailItem.innerHTML = `<strong>步骤 ${index + 1}:</strong> ${this.escapeHtml(detail)}`;
|
||||
detailsContent.appendChild(detailItem);
|
||||
});
|
||||
|
||||
// 点击切换折叠状态
|
||||
detailsToggle.addEventListener('click', () => {
|
||||
detailsContent.classList.toggle('expanded');
|
||||
detailsToggle.classList.toggle('expanded');
|
||||
});
|
||||
|
||||
detailsContainer.appendChild(detailsToggle);
|
||||
detailsContainer.appendChild(detailsContent);
|
||||
}
|
||||
|
||||
// 更新主要响应内容(使用Markdown渲染)
|
||||
console.log('开始渲染 Markdown');
|
||||
const renderedHtml = this.renderMarkdown(response);
|
||||
console.log('Markdown 渲染完成,HTML 长度:', renderedHtml ? renderedHtml.length : 0);
|
||||
messageContent.innerHTML = renderedHtml;
|
||||
console.log('innerHTML 已设置');
|
||||
// 高亮代码块
|
||||
this.highlightCodeBlocks(messageContent);
|
||||
console.log('代码块高亮完成');
|
||||
|
||||
// 保存到历史记录
|
||||
this.currentChatHistory.push({
|
||||
type: 'assistant',
|
||||
content: response,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
this.scrollToBottom();
|
||||
return messageElement;
|
||||
}
|
||||
|
||||
// 添加智能运维消息(带折叠详情)- 保留用于兼容性
|
||||
addAIOpsMessage(response, details) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message assistant aiops-message';
|
||||
|
||||
// 添加头像图标
|
||||
const messageAvatar = document.createElement('div');
|
||||
messageAvatar.className = 'message-avatar';
|
||||
messageAvatar.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" fill="white"/>
|
||||
</svg>
|
||||
`;
|
||||
messageDiv.appendChild(messageAvatar);
|
||||
|
||||
// 创建消息内容包装器
|
||||
const messageContentWrapper = document.createElement('div');
|
||||
messageContentWrapper.className = 'message-content-wrapper';
|
||||
|
||||
// 详情部分(可折叠)- 先显示
|
||||
if (details && details.length > 0) {
|
||||
const detailsContainer = document.createElement('div');
|
||||
detailsContainer.className = 'aiops-details';
|
||||
|
||||
const detailsToggle = document.createElement('div');
|
||||
detailsToggle.className = 'details-toggle';
|
||||
detailsToggle.innerHTML = `
|
||||
<svg class="toggle-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>查看详细步骤 (${details.length}条)</span>
|
||||
`;
|
||||
|
||||
const detailsContent = document.createElement('div');
|
||||
detailsContent.className = 'details-content';
|
||||
|
||||
details.forEach((detail, index) => {
|
||||
const detailItem = document.createElement('div');
|
||||
detailItem.className = 'detail-item';
|
||||
detailItem.innerHTML = `<strong>步骤 ${index + 1}:</strong> ${this.escapeHtml(detail)}`;
|
||||
detailsContent.appendChild(detailItem);
|
||||
});
|
||||
|
||||
// 点击切换折叠状态
|
||||
detailsToggle.addEventListener('click', () => {
|
||||
detailsContent.classList.toggle('expanded');
|
||||
detailsToggle.classList.toggle('expanded');
|
||||
});
|
||||
|
||||
detailsContainer.appendChild(detailsToggle);
|
||||
detailsContainer.appendChild(detailsContent);
|
||||
messageContentWrapper.appendChild(detailsContainer);
|
||||
}
|
||||
|
||||
// 主要响应内容 - 后显示(使用Markdown渲染)
|
||||
const messageContent = document.createElement('div');
|
||||
messageContent.className = 'message-content';
|
||||
messageContent.innerHTML = this.renderMarkdown(response);
|
||||
// 高亮代码块
|
||||
this.highlightCodeBlocks(messageContent);
|
||||
messageContentWrapper.appendChild(messageContent);
|
||||
messageDiv.appendChild(messageContentWrapper);
|
||||
|
||||
if (this.chatMessages) {
|
||||
this.chatMessages.appendChild(messageDiv);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
return messageDiv;
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
@@ -1431,52 +1082,16 @@ class SuperBizAgentApp {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 触发智能运维(点击智能运维按钮时直接调用)
|
||||
async triggerAIOps() {
|
||||
if (this.isStreaming) {
|
||||
this.showNotification('请等待当前操作完成', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 新建对话
|
||||
this.newChat();
|
||||
|
||||
// 添加"分析中..."的消息(带旋转动画)
|
||||
const loadingMessage = this.addLoadingMessage('分析中...');
|
||||
this.currentAIOpsMessage = loadingMessage; // 保存消息引用用于后续更新
|
||||
|
||||
// 设置发送状态
|
||||
this.isStreaming = true;
|
||||
this.updateUI();
|
||||
|
||||
try {
|
||||
await this.sendAIOpsRequest(loadingMessage);
|
||||
} catch (error) {
|
||||
console.error('智能运维分析失败:', error);
|
||||
// 更新消息为错误信息
|
||||
if (loadingMessage) {
|
||||
const messageContent = loadingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = '抱歉,智能运维分析时出现错误:' + error.message;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isStreaming = false;
|
||||
this.currentAIOpsMessage = null;
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示/隐藏加载遮罩层
|
||||
showLoadingOverlay(show) {
|
||||
if (this.loadingOverlay) {
|
||||
if (show) {
|
||||
this.loadingOverlay.style.display = 'flex';
|
||||
// 更新文字为智能运维
|
||||
// 更新文字为 Chat 处理状态
|
||||
const loadingText = this.loadingOverlay.querySelector('.loading-text');
|
||||
const loadingSubtext = this.loadingOverlay.querySelector('.loading-subtext');
|
||||
if (loadingText) loadingText.textContent = '智能运维分析中,请稍候...';
|
||||
if (loadingSubtext) loadingSubtext.textContent = '后端正在处理,请耐心等待';
|
||||
if (loadingText) loadingText.textContent = '正在处理,请稍候...';
|
||||
if (loadingSubtext) loadingSubtext.textContent = '后端正在准备安全回答';
|
||||
// 防止页面滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
|
||||
@@ -57,14 +57,6 @@
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="main-content">
|
||||
<button class="ai-ops-top-btn" id="aiOpsSidebarBtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>AI Ops</span>
|
||||
</button>
|
||||
<div class="chat-container">
|
||||
<div class="welcome-greeting" id="welcomeGreeting">
|
||||
<p>你好!我是智能Oncall小助手</p>
|
||||
@@ -114,8 +106,8 @@
|
||||
<div id="loadingOverlay" class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">智能运维分析中,请稍候...</div>
|
||||
<div class="loading-subtext">后端正在处理,请耐心等待</div>
|
||||
<div class="loading-text">正在处理,请稍候...</div>
|
||||
<div class="loading-subtext">后端正在准备安全回答</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -210,39 +210,6 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* AI Ops按钮 - 右上角 */
|
||||
.ai-ops-top-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #ff9800;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 4px rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.ai-ops-top-btn:hover {
|
||||
background: #fb8c00;
|
||||
box-shadow: 0 4px 8px rgba(255, 152, 0, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ai-ops-top-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -879,99 +846,6 @@ body {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 智能运维消息样式 */
|
||||
.aiops-message {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.aiops-message .message-content {
|
||||
max-width: 100%;
|
||||
background: rgba(26, 115, 232, 0.05);
|
||||
border: 1px solid rgba(26, 115, 232, 0.2);
|
||||
}
|
||||
|
||||
/* 详情折叠区域 */
|
||||
.aiops-details {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.details-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(26, 115, 232, 0.05);
|
||||
border: 1px solid rgba(26, 115, 232, 0.2);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
color: #1a73e8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.details-toggle:hover {
|
||||
background: rgba(26, 115, 232, 0.1);
|
||||
border-color: rgba(26, 115, 232, 0.3);
|
||||
}
|
||||
|
||||
.details-toggle.expanded {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.details-toggle.expanded .toggle-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.details-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(26, 115, 232, 0.2);
|
||||
border-top: none;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
.details-content.expanded {
|
||||
max-height: 2000px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
background: rgba(26, 115, 232, 0.03);
|
||||
border-left: 3px solid #1a73e8;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: #202124;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-item strong {
|
||||
color: #1a73e8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 加载遮罩层样式 */
|
||||
.loading-overlay {
|
||||
display: none;
|
||||
|
||||
Reference in New Issue
Block a user