Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
Ftt_Common
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Ftt_Common
Commits
12f2ed0d
Commit
12f2ed0d
authored
Sep 26, 2026
by
段廷瑞
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
pref: Agent可观测性优化(命令:/stats /admin UI: 设置-统计看板)
parent
5b9144c4
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
322 additions
and
47 deletions
+322
-47
Tools.Agent.dll
AppHome/Tools.Agent.dll
+0
-0
Tools.Agent.xml
AppHome/Tools.Agent.xml
+53
-3
AgentEngine.cs
Tools.Agent/Agent/AgentEngine.cs
+20
-3
OpenAiCompatibleClient.cs
Tools.Agent/Agent/OpenAiCompatibleClient.cs
+1
-0
AgentRuntime.cs
Tools.Agent/AgentRuntime.cs
+2
-2
AgentAdminForm.cs
Tools.Agent/Forms/AgentAdminForm.cs
+59
-31
KbService.cs
Tools.Agent/Kb/KbService.cs
+1
-0
AgentLogger.cs
Tools.Agent/Support/AgentLogger.cs
+40
-3
AgentTelemetry.cs
Tools.Agent/Support/AgentTelemetry.cs
+128
-4
wMyAgent.cs
Tools.Agent/wMyAgent.cs
+18
-1
No files found.
AppHome/Tools.Agent.dll
View file @
12f2ed0d
No preview for this file type
AppHome/Tools.Agent.xml
View file @
12f2ed0d
...
...
@@ -43,7 +43,7 @@
<member
name=
"M:Tools.MyAgent.AgentRuntime.CreateEngine(Tools.MyAgent.Kb.KbService)"
>
<summary>
创建无 UI Agent 引擎。调用方负责配置、订阅事件和释放。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentRuntime.CreateAdminForm(System.Int32,System.Boolean)"
>
<member
name=
"M:Tools.MyAgent.AgentRuntime.CreateAdminForm(System.Int32,System.Boolean
,System.Boolean
)"
>
<summary>
创建 Admin 看板窗体(供宿主挂到菜单/工具栏)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.AgentProductContext"
>
...
...
@@ -1223,6 +1223,9 @@
<para>
与聊天窗
<c>
/stats
</c>
<c>
/diag
</c>
互补:那两条是终端用户随口问,这里是管理者看趋势。
</para>
</summary>
</member>
<member
name=
"M:Tools.MyAgent.Forms.AgentAdminForm.Summarize(System.Collections.Generic.List{Tools.MyAgent.TelemetryStore.RunRow},System.String)"
>
<summary>
一组运行记录的 KPI 摘要行(概览双窗口共用)。
</summary>
</member>
<member
name=
"T:Tools.MyAgent.Forms.DashChart"
>
<summary>
看板自绘图表(柱状 / 横向条形)。零第三方依赖:GDI+ 直接画,约 120 行。
...
...
@@ -2267,8 +2270,23 @@
<para>
落盘前统一脱敏:手机号 / 邮箱 / API Key / 密码 / 令牌。
</para>
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentLogger.MaxLogBytes"
>
<summary>
单个日志文件的轮转阈值(8MB,与观测 jsonl 一致 —— G6)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentLogger.Log(System.String)"
>
<summary>
追加一行日志(自动脱敏 + 时间戳)
</summary>
<summary>
追加一行 INFO 级日志(自动脱敏 + 时间戳 + [INF] 前缀)
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentLogger.LogWarning(System.String)"
>
<summary>
追加一行 WARN 级日志(G6:日志分级)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentLogger.LogError(System.String)"
>
<summary>
追加一行 ERROR 级日志(G6:日志分级)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentLogger.Write(System.String,System.String)"
>
<summary>
实际写行:时间戳 + [级别] + 脱敏正文。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentLogger.NextLogFile"
>
<summary>
当日日志文件(8MB 轮转:agent_yyyyMMdd.log → _1 → _2…)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentLogger.LogCall(System.String,System.Int32,System.Int32,System.Int64,System.String,System.String,System.String)"
>
<summary>
模型调用记录(#29):model / token / 耗时 / 工具序列 / 结果
</summary>
...
...
@@ -2303,6 +2321,9 @@
<member
name=
"F:Tools.MyAgent.AgentTelemetry.RunScope.QuestionHash"
>
<summary>
提问哈希(SHA256 前 12 位;一轮算一次,非 hot path)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry.RunScope.QuestionPreview"
>
<summary>
提问前 120 字(仅 detailed 级别填充;basic 恒空 —— §9 隐私分级)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry.RunScope.Model"
>
<summary>
配置模型。
</summary>
</member>
...
...
@@ -2336,6 +2357,9 @@
<member
name=
"F:Tools.MyAgent.AgentTelemetry.ToolSpan.ArgsSummary"
>
<summary>
参数摘要(经
<see
cref=
"M:Tools.MyAgent.AgentLogger.Mask(System.String)"
/>
后截断 80 字)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry.ToolSpan.ResultPreview"
>
<summary>
工具返回前 200 字(仅 detailed 级别填充;basic 恒空 —— §9 隐私分级)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry.ToolSpan.StartOffsetMs"
>
<summary>
相对本轮起点的偏移(ms)。
</summary>
</member>
...
...
@@ -2357,6 +2381,12 @@
<member
name=
"T:Tools.MyAgent.AgentTelemetry.RunStats"
>
<summary>
内存快照(
<c>
/stats
</c>
与看板读这个)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry.RunStats.QueueDepth"
>
<summary>
gauge:待落盘队列当前深度(观测自查)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry.RunStats.LogSizeToday"
>
<summary>
gauge:Logs 目录当日观测文件总字节数(runs/metrics jsonl,观测自查)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry.RunStats.Counters"
>
<summary>
计数器全量副本(键形如
<c>
agent_runs_total|outcome=success
</c>
)。
</summary>
</member>
...
...
@@ -2404,6 +2434,9 @@
<member
name=
"M:Tools.MyAgent.AgentTelemetry.Get(System.String)"
>
<summary>
取计数器值(不存在返回 0)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentTelemetry.LogsSizeToday"
>
<summary>
gauge:当日观测文件(runs/metrics jsonl)总字节数,供观测自查。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentTelemetry.SumPrefix(System.String)"
>
<summary>
按前缀汇总(用于无维度的总量)。
</summary>
</member>
...
...
@@ -2432,7 +2465,21 @@
<summary>
一轮运行入队(满则丢弃并计数)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentTelemetry.FlushMetrics"
>
<summary>
落一条指标快照(计数器 + 直方图读数)。
</summary>
<summary>
落一条指标快照(计数器 + 直方图读数;直方图带原始桶分布,供重启重放精确合并)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentTelemetry.HistSnapshot(System.String)"
>
<summary>
单个直方图的落盘形态:count/sum/min/max + 12 桶分布(P1-6 重放合并的数据源)。
</summary>
</member>
<member
name=
"F:Tools.MyAgent.AgentTelemetry._replayed"
>
<summary>
P1-6:启动时重放当日 metrics_*.jsonl 最后一条快照并合并进内存,
使
<c>
/stats
</c>
与设置页 KPI 在进程重启后不丢当日累计(看板读 jsonl 本就不受影响)。
<para>
合并语义:计数器 = 存量 + 当前(重放只在启动时做一次,此刻当前≈0);
直方图按桶分布相加。旧格式快照(无 buckets)跳过直方图部分。
</para>
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentTelemetry.MergeHist(Newtonsoft.Json.Linq.JToken,System.String)"
>
<summary>
把一条落盘直方图快照按桶合并进内存(count/sum/min/max + buckets)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.AgentTelemetry.Flush"
>
<summary>
把队列与缓冲落盘(进程退出 / 手动调用)。
</summary>
...
...
@@ -3301,6 +3348,9 @@
<member
name=
"M:Tools.MyAgent.wMyAgent.SaveAgentConfig(Tools.MyAgent.Config.AgentConfig)"
>
<summary>
保存 Agent 配置文件到默认 Agent 目录下 config 文件夹(全部凭据经 DPAPI 加密落盘:共享密码 / ApiKey / EmbeddingApiKey / BackupApiKey / 预存连接串)
</summary>
</member>
<member
name=
"M:Tools.MyAgent.wMyAgent.RunStartupHealthCheck"
>
<summary>
P1-4:HealthCheckOnStart=true 时启动后台被动自检并落 health.json(不做模型服务网络探测,避免拖慢启动)。
</summary>
</member>
<member
name=
"M:Tools.MyAgent.wMyAgent.WorkingDirForHistory"
>
<summary>
历史记录固定保存到默认 Agent 目录(与所选知识库无关)
</summary>
</member>
...
...
Tools.Agent/Agent/AgentEngine.cs
View file @
12f2ed0d
...
...
@@ -343,6 +343,7 @@ namespace Tools.MyAgent.Agent
IsRunning
=
true
;
_cts
=
CancellationTokenSource
.
CreateLinkedTokenSource
(
ct
);
OnStatusChanged
?.
Invoke
();
int
toolRounds
=
0
;
// 提升到 try 外:catch 路径的 EndRun 也要带回 ToolRounds
try
{
...
...
@@ -355,11 +356,15 @@ namespace Tools.MyAgent.Agent
_answerKind
=
AnswerKind
.
Knowledge
;
_currentQuestion
=
LastUserQuestion
(
history
);
_runScope
=
AgentTelemetry
.
BeginRun
(
_currentQuestion
.
Length
,
AgentTelemetry
.
HashQuestion
(
_currentQuestion
),
_client
.
Model
);
if
(
_runScope
!=
null
)
_runScope
.
RunId
=
_diagnosticSessionId
;
if
(
_runScope
!=
null
)
{
_runScope
.
RunId
=
_diagnosticSessionId
;
if
(
AgentTelemetry
.
IsDetailed
&&
!
string
.
IsNullOrEmpty
(
_currentQuestion
))
// §9:仅 detailed 存提问前 120 字
_runScope
.
QuestionPreview
=
_currentQuestion
.
Substring
(
0
,
Math
.
Min
(
120
,
_currentQuestion
.
Length
));
}
var
messages
=
await
BuildMessagesAsync
(
history
,
_cts
.
Token
).
ConfigureAwait
(
false
);
var
tools
=
BuildTools
();
var
stopwatch
=
Stopwatch
.
StartNew
();
int
toolRounds
=
0
;
var
finalAnswer
=
new
StringBuilder
();
// 跨轮累计的回答正文(UI 事件为全量替换语义)
// P3 单轮直答(QuickAnswer):本地召回命中知识库时,注入证据做一次「无工具」补全即收尾,
...
...
@@ -444,6 +449,7 @@ namespace Tools.MyAgent.Agent
if
(!
IsDisposed
)
{
if
(
_runScope
!=
null
)
{
_runScope
.
ToolRounds
=
toolRounds
;
_runScope
.
AnswerKind
=
_answerKind
.
ToString
();
}
AgentTelemetry
.
EndRun
(
_runScope
,
"success"
,
""
,
null
);
OnComplete
?.
Invoke
();
if
(
_toolTrace
.
Count
>
0
)
...
...
@@ -452,6 +458,7 @@ namespace Tools.MyAgent.Agent
}
catch
(
OperationCanceledException
)
{
if
(
_runScope
!=
null
)
{
_runScope
.
ToolRounds
=
toolRounds
;
_runScope
.
AnswerKind
=
_answerKind
.
ToString
();
}
AgentTelemetry
.
EndRun
(
_runScope
,
"cancel"
,
""
,
null
);
if
(!
IsDisposed
)
{
...
...
@@ -465,6 +472,7 @@ namespace Tools.MyAgent.Agent
}
catch
(
Exception
ex
)
{
if
(
_runScope
!=
null
)
{
_runScope
.
ToolRounds
=
toolRounds
;
_runScope
.
AnswerKind
=
_answerKind
.
ToString
();
}
AgentTelemetry
.
EndRun
(
_runScope
,
"error"
,
ClassifyError
(
ex
),
ex
);
if
(!
IsDisposed
)
{
...
...
@@ -524,7 +532,10 @@ namespace Tools.MyAgent.Agent
// 部分命中时先明确告知「含模型补充内容」,避免用户把补充内容当成知识库原文
if
(!
string
.
IsNullOrEmpty
(
recall
.
Reason
))
OnTextDelta
?.
Invoke
(
"\n\n"
+
recall
.
Reason
);
AgentLogger
.
Log
(
$"本地召回命中:mode=
{
recall
.
Mode
}
score=
{
recall
.
Primary
.
Score
:
0.00
}
file=
{
recall
.
Primary
.
RelPath
}
kw=
{
string
.
Join
(
"|"
,
recall
.
Keywords
)}
"
);
// G7 隐私:关键词源自提问原文,basic 级别只记个数,detailed 才记关键词(与遥测隐私分级一致)
AgentLogger
.
Log
(
AgentTelemetry
.
IsDetailed
?
$"本地召回命中:mode=
{
recall
.
Mode
}
score=
{
recall
.
Primary
.
Score
:
0.00
}
file=
{
recall
.
Primary
.
RelPath
}
kw=
{
string
.
Join
(
"|"
,
recall
.
Keywords
)}
"
:
$"本地召回命中:mode=
{
recall
.
Mode
}
score=
{
recall
.
Primary
.
Score
:
0.00
}
file=
{
recall
.
Primary
.
RelPath
}
kwCount=
{
recall
.
Keywords
.
Count
}
"
);
var
result
=
await
SendAndLogAsync
(
messages
,
null
,
ct
,
"direct"
).
ConfigureAwait
(
false
);
if
(
result
.
ReasoningContent
.
Length
>
0
)
...
...
@@ -684,6 +695,8 @@ namespace Tools.MyAgent.Agent
&&
_continueRounds
<
MaxContinuationRounds
)
{
_continueRounds
++;
if
(
_runScope
!=
null
)
_runScope
.
ContinuationCount
=
_continueRounds
;
AgentTelemetry
.
Count
(
"agent_continuation_total"
);
// §6 #7:续写触发(输出上限截断)
AppendContinuationMessages
(
messages
,
result
);
OnTextDelta
?.
Invoke
(
finalAnswer
.
ToString
());
AgentLogger
.
Log
(
$"回答被输出上限(max_tokens)截断,自动续写第
{
_continueRounds
}
/
{
MaxContinuationRounds
}
轮"
);
...
...
@@ -849,6 +862,8 @@ namespace Tools.MyAgent.Agent
int
dropped
=
DropOldestUntilUnder
(
list
,
budget
);
if
(
dropped
>
0
)
{
AgentTelemetry
.
Count
(
"agent_context_trim_total"
,
dropped
);
// §6 #7:上下文裁剪(含条数)
if
(
_runScope
!=
null
)
_runScope
.
ContextTrimCount
+=
dropped
;
_contextNotice
=
$"(对话历史较长,已自动省略最早
{
dropped
}
条消息以控制上下文)"
;
OnTextDelta
?.
Invoke
(
"\n\n"
+
_contextNotice
);
}
...
...
@@ -1272,6 +1287,8 @@ namespace Tools.MyAgent.Agent
DurationMs
=
sw
.
ElapsedMilliseconds
,
Outcome
=
"ok"
,
ResultChars
=
(
result
??
""
).
Length
,
ResultPreview
=
AgentTelemetry
.
IsDetailed
&&
!
string
.
IsNullOrEmpty
(
result
)
?
result
.
Substring
(
0
,
Math
.
Min
(
200
,
result
.
Length
))
:
""
,
// §9:仅 detailed
Truncated
=
result
!=
null
&&
result
.
Length
>
MaxToolReturnChars
,
});
// #42 结果压缩:超长结果压缩后回填模型,完整结果落盘供溯源
...
...
Tools.Agent/Agent/OpenAiCompatibleClient.cs
View file @
12f2ed0d
...
...
@@ -425,6 +425,7 @@ namespace Tools.MyAgent.Agent
/// <summary>指数退避 + 抖动(约 1s / 2s,封顶 4s),可被用户取消</summary>
private
static
async
Task
RetryDelayAsync
(
int
attempt
,
CancellationToken
ct
)
{
AgentTelemetry
.
Count
(
"agent_http_retry_total"
);
// 网络抖动强度(超时/429/5xx/流中断统一计)
int
delayMs
;
lock
(
RetryJitter
)
{
...
...
Tools.Agent/AgentRuntime.cs
View file @
12f2ed0d
...
...
@@ -101,9 +101,9 @@ namespace Tools.MyAgent
}
/// <summary>创建 Admin 看板窗体(供宿主挂到菜单/工具栏)。</summary>
public
Form
CreateAdminForm
(
int
days
=
7
,
bool
allowDetailed
=
false
)
public
Form
CreateAdminForm
(
int
days
=
7
,
bool
allowDetailed
=
false
,
bool
autoRefresh
=
false
)
{
return
new
Tools
.
MyAgent
.
Forms
.
AgentAdminForm
(
null
,
days
,
allowDetailed
);
return
new
Tools
.
MyAgent
.
Forms
.
AgentAdminForm
(
null
,
days
,
allowDetailed
,
autoRefresh
);
}
private
void
EnsureDefaultResources
()
...
...
Tools.Agent/Forms/AgentAdminForm.cs
View file @
12f2ed0d
...
...
@@ -34,7 +34,7 @@ namespace Tools.MyAgent.Forms
private
readonly
Timer
_timer
=
new
Timer
();
private
readonly
Label
_hint
=
new
Label
();
public
AgentAdminForm
(
string
logsRoot
=
null
,
int
days
=
7
,
bool
allowDetailed
=
false
)
public
AgentAdminForm
(
string
logsRoot
=
null
,
int
days
=
7
,
bool
allowDetailed
=
false
,
bool
autoRefresh
=
false
)
{
_allowDetailed
=
allowDetailed
;
_store
=
new
TelemetryStore
(
logsRoot
,
days
)
{
AllowDetailed
=
allowDetailed
};
...
...
@@ -73,6 +73,8 @@ namespace Tools.MyAgent.Forms
_timer
.
Interval
=
30000
;
_timer
.
Tick
+=
(
s
,
e
)
=>
LoadData
();
if
(
autoRefresh
)
chkAuto
.
Checked
=
true
;
// P1-5:AdminAutoRefresh 接线(CheckedChanged 已启用 Timer)
Controls
.
Add
(
_tabs
);
Controls
.
Add
(
top
);
...
...
@@ -206,38 +208,13 @@ namespace Tools.MyAgent.Forms
private
void
BindOverview
()
{
int
runs
=
_runs
.
Length
;
int
ok
=
0
,
err
=
0
,
cancel
=
0
;
long
tokens
=
0
;
double
cost
=
0
;
int
tools
=
0
,
llms
=
0
;
// P2-11:今日 vs 近 7 天 双窗口(原只有全期合计,看不出"今天用量多少")
var
today
=
DateTime
.
Now
.
Date
;
var
todays
=
new
System
.
Collections
.
Generic
.
List
<
TelemetryStore
.
RunRow
>()
;
foreach
(
var
r
in
_runs
)
{
if
(
r
.
Outcome
==
"success"
)
ok
++;
else
if
(
r
.
Outcome
==
"error"
)
err
++;
else
cancel
++;
tokens
+=
r
.
PromptTokens
+
r
.
CompletionTokens
;
cost
+=
r
.
Cost
;
tools
+=
r
.
ToolCalls
;
llms
+=
r
.
LlmCalls
;
}
long
p50
=
0
,
p95
=
0
;
var
ts
=
new
System
.
Collections
.
Generic
.
List
<
long
>();
foreach
(
var
r
in
_runs
)
ts
.
Add
(
r
.
TotalMs
);
ts
.
Sort
();
if
(
ts
.
Count
>
0
)
{
p50
=
ts
[(
int
)
Math
.
Ceiling
(
ts
.
Count
*
0.5
)
-
1
];
p95
=
ts
[(
int
)
Math
.
Ceiling
(
ts
.
Count
*
0.95
)
-
1
];
}
double
rate
=
runs
==
0
?
0
:
ok
*
100.0
/
runs
;
double
perRun
=
runs
==
0
?
0
:
(
double
)
llms
/
runs
;
if
(
r
.
StartedAt
>=
today
)
todays
.
Add
(
r
);
_kpi
.
Text
=
string
.
Format
(
"轮数 {0} 成功率 {1:F1}% 失败 {2} / 取消 {3}\n"
+
"耗时 P50 {4}ms · P95 {5}ms 工具 {6} 次 · LLM {7} 次(均 {8:F1} 次/轮)\n"
+
"Token {9} 费用 {10} 直答率 {11:F1}%"
,
runs
,
rate
,
err
,
cancel
,
p50
,
p95
,
tools
,
llms
,
perRun
,
tokens
,
cost
>
0
?
CostEstimator
.
Format
(
cost
)
:
"—"
,
_store
.
AggregateModes
(
_runs
).
DirectRate
);
_kpi
.
Text
=
Summarize
(
todays
,
"今日"
)
+
"\n"
+
Summarize
(
new
System
.
Collections
.
Generic
.
List
<
TelemetryStore
.
RunRow
>(
_runs
),
"近 7 天"
);
long
[]
failed
;
long
[]
okHours
=
_store
.
Hourly
(
_runs
,
out
failed
);
...
...
@@ -258,6 +235,41 @@ namespace Tools.MyAgent.Forms
_hourly
.
Invalidate
();
}
/// <summary>一组运行记录的 KPI 摘要行(概览双窗口共用)。</summary>
private
string
Summarize
(
System
.
Collections
.
Generic
.
List
<
TelemetryStore
.
RunRow
>
rows
,
string
label
)
{
try
{
int
runs
=
rows
.
Count
;
if
(
runs
==
0
)
return
label
+
":无数据"
;
int
ok
=
0
,
err
=
0
,
cancel
=
0
,
tools
=
0
,
llms
=
0
;
long
tokens
=
0
,
p95
=
0
;
double
cost
=
0
;
var
ts
=
new
System
.
Collections
.
Generic
.
List
<
long
>();
foreach
(
var
r
in
rows
)
{
if
(
r
.
Outcome
==
"success"
)
ok
++;
else
if
(
r
.
Outcome
==
"error"
)
err
++;
else
cancel
++;
tools
+=
r
.
ToolCalls
;
llms
+=
r
.
LlmCalls
;
tokens
+=
r
.
PromptTokens
+
r
.
CompletionTokens
;
cost
+=
r
.
Cost
;
ts
.
Add
(
r
.
TotalMs
);
}
ts
.
Sort
();
if
(
ts
.
Count
>
0
)
p95
=
ts
[(
int
)
Math
.
Ceiling
(
ts
.
Count
*
0.95
)
-
1
];
double
direct
=
_store
.
AggregateModes
(
rows
.
ToArray
()).
DirectRate
;
return
string
.
Format
(
"{0}:轮数 {1}(成功率 {2:F1}% · 失败 {3} · 取消 {4})· P95 {5}ms · 工具 {6} · LLM {7}(均 {8:F1}/轮)· 直答率 {9:F1}%\n"
+
" Token {10} · 费用 {11}"
,
label
,
runs
,
ok
*
100.0
/
runs
,
err
,
cancel
,
p95
,
tools
,
llms
,
runs
==
0
?
0
:
(
double
)
llms
/
runs
,
direct
,
tokens
,
cost
>
0
?
CostEstimator
.
Format
(
cost
)
:
"—"
);
}
catch
{
return
label
+
":—"
;
}
}
private
void
BindTimeline
()
{
_gridTime
.
Columns
.
Clear
();
...
...
@@ -339,6 +351,22 @@ namespace Tools.MyAgent.Forms
_gridKb
.
Rows
.
Add
(
"model 未召回"
,
m
.
Model
,
"知识库未命中"
);
_gridKb
.
Rows
.
Add
(
"tool-loop 工具循环"
,
m
.
ToolLoop
,
"多轮工具检索"
);
_gridKb
.
Rows
.
Add
(
"检索零命中"
,
AgentTelemetry
.
Get
(
"agent_kb_zero_hit_total"
),
"进程内累计(知识缺口信号)"
);
// P2-12:TOP 未命中关键词仅 detailed 可见(查询词源自提问原文,受隐私闸门约束)
if
(
_allowDetailed
)
{
try
{
var
misses
=
Tools
.
MyAgent
.
Kb
.
KbService
.
RecentMisses
(
7
,
10
);
if
(
misses
.
Count
>
0
)
{
_gridKb
.
Rows
.
Add
(
"── TOP 未命中关键词(近 7 天)──"
,
"次数"
,
"来源:search_misses.csv"
);
foreach
(
var
miss
in
misses
)
_gridKb
.
Rows
.
Add
(
miss
.
Query
,
miss
.
Count
,
"0 命中"
);
}
}
catch
{
}
}
}
private
void
BindErrors
()
...
...
Tools.Agent/Kb/KbService.cs
View file @
12f2ed0d
...
...
@@ -2208,6 +2208,7 @@ namespace Tools.MyAgent.Kb
{
try
{
AgentTelemetry
.
Count
(
"agent_kb_zero_hit_total"
);
// 知识缺口信号(看板④Tab 读取)
Directory
.
CreateDirectory
(
Path
.
GetDirectoryName
(
MissLogPath
));
string
line
=
string
.
Join
(
","
,
CsvQuote
(
DateTime
.
Now
.
ToString
(
"yyyy-MM-dd HH:mm:ss"
)),
...
...
Tools.Agent/Support/AgentLogger.cs
View file @
12f2ed0d
...
...
@@ -16,22 +16,59 @@ namespace Tools.MyAgent
private
static
string
LogDir
=>
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Logs"
);
/// <summary>追加一行日志(自动脱敏 + 时间戳)</summary>
/// <summary>单个日志文件的轮转阈值(8MB,与观测 jsonl 一致 —— G6)。</summary>
private
const
long
MaxLogBytes
=
8L
*
1024
*
1024
;
/// <summary>追加一行 INFO 级日志(自动脱敏 + 时间戳 + [INF] 前缀)</summary>
public
static
void
Log
(
string
message
)
{
try
{
Write
(
"INF"
,
message
);
}
catch
{
}
}
/// <summary>追加一行 WARN 级日志(G6:日志分级)。</summary>
public
static
void
LogWarning
(
string
message
)
{
try
{
Write
(
"WRN"
,
message
);
}
catch
{
}
}
/// <summary>追加一行 ERROR 级日志(G6:日志分级)。</summary>
public
static
void
LogError
(
string
message
)
{
try
{
Write
(
"ERR"
,
message
);
}
catch
{
}
}
/// <summary>实际写行:时间戳 + [级别] + 脱敏正文。</summary>
private
static
void
Write
(
string
level
,
string
message
)
{
try
{
lock
(
Lock
)
{
Directory
.
CreateDirectory
(
LogDir
);
string
file
=
Path
.
Combine
(
LogDir
,
$"agent_
{
DateTime
.
Now
:
yyyyMMdd
}
.log"
);
string
line
=
$"
{
DateTime
.
Now
:
yyyy
-
MM
-
dd
HH
:
mm
:
ss
.
fff
}
\t
{
Mask
(
message
)}
\n"
;
string
file
=
NextLogFile
(
);
string
line
=
$"
{
DateTime
.
Now
:
yyyy
-
MM
-
dd
HH
:
mm
:
ss
.
fff
}
\t
[
{
level
}
]\t
{
Mask
(
message
)}
\n"
;
File
.
AppendAllText
(
file
,
line
,
Encoding
.
UTF8
);
}
}
catch
{
}
}
/// <summary>当日日志文件(8MB 轮转:agent_yyyyMMdd.log → _1 → _2…)。</summary>
private
static
string
NextLogFile
()
{
string
primary
=
Path
.
Combine
(
LogDir
,
$"agent_
{
DateTime
.
Now
:
yyyyMMdd
}
.log"
);
if
(!
File
.
Exists
(
primary
)
||
new
FileInfo
(
primary
).
Length
<
MaxLogBytes
)
return
primary
;
for
(
int
i
=
1
;
i
<
100
;
i
++)
{
string
candidate
=
Path
.
Combine
(
LogDir
,
$"agent_
{
DateTime
.
Now
:
yyyyMMdd
}
_
{
i
}
.log"
);
if
(!
File
.
Exists
(
candidate
)
||
new
FileInfo
(
candidate
).
Length
<
MaxLogBytes
)
return
candidate
;
}
return
primary
;
}
/// <summary>模型调用记录(#29):model / token / 耗时 / 工具序列 / 结果</summary>
public
static
void
LogCall
(
string
model
,
int
promptTokens
,
int
completionTokens
,
long
elapsedMs
,
string
tools
,
string
note
,
string
traceId
=
""
)
{
...
...
Tools.Agent/Support/AgentTelemetry.cs
View file @
12f2ed0d
using
Newtonsoft.Json
;
using
Newtonsoft.Json.Linq
;
using
System
;
using
System.Collections.Concurrent
;
using
System.Collections.Generic
;
...
...
@@ -35,6 +36,9 @@ namespace Tools.MyAgent
/// <summary>提问哈希(SHA256 前 12 位;一轮算一次,非 hot path)。</summary>
public
string
QuestionHash
=
""
;
/// <summary>提问前 120 字(仅 detailed 级别填充;basic 恒空 —— §9 隐私分级)。</summary>
public
string
QuestionPreview
=
""
;
/// <summary>配置模型。</summary>
public
string
Model
=
""
;
...
...
@@ -98,6 +102,9 @@ namespace Tools.MyAgent
/// <summary>参数摘要(经 <see cref="AgentLogger.Mask"/> 后截断 80 字)。</summary>
public
string
ArgsSummary
=
""
;
/// <summary>工具返回前 200 字(仅 detailed 级别填充;basic 恒空 —— §9 隐私分级)。</summary>
public
string
ResultPreview
=
""
;
/// <summary>相对本轮起点的偏移(ms)。</summary>
public
long
StartOffsetMs
;
...
...
@@ -164,6 +171,13 @@ namespace Tools.MyAgent
public
long
CompletionTokens
;
public
double
Cost
;
public
long
Dropped
;
/// <summary>gauge:待落盘队列当前深度(观测自查)。</summary>
public
long
QueueDepth
;
/// <summary>gauge:Logs 目录当日观测文件总字节数(runs/metrics jsonl,观测自查)。</summary>
public
long
LogSizeToday
;
public
HistogramView
Duration
=
new
HistogramView
();
public
HistogramView
Ttfb
=
new
HistogramView
();
...
...
@@ -219,7 +233,11 @@ namespace Tools.MyAgent
:
"basic"
;
if
(!
string
.
IsNullOrEmpty
(
logsRoot
))
_logsRoot
=
logsRoot
;
}
if
(
_enabled
&&
!
string
.
Equals
(
_level
,
"off"
,
StringComparison
.
OrdinalIgnoreCase
))
EnsureWriter
();
if
(
_enabled
&&
!
string
.
Equals
(
_level
,
"off"
,
StringComparison
.
OrdinalIgnoreCase
))
{
ReplayTodayMetrics
();
// P1-6:先重放(此刻内存≈0,合并=纯叠加),再启动写线程
EnsureWriter
();
}
}
catch
{
}
}
...
...
@@ -429,6 +447,8 @@ namespace Tools.MyAgent
if
(
kv
.
Key
.
StartsWith
(
"agent_cost_total"
,
StringComparison
.
OrdinalIgnoreCase
))
costMilli
+=
kv
.
Value
;
}
st
.
Cost
=
costMilli
/
10000.0
;
// 计数器以「毫分」存费用
st
.
QueueDepth
=
RunQueue
.
Count
;
// gauge:队列深度
st
.
LogSizeToday
=
LogsSizeToday
();
return
st
;
}
catch
{
return
new
RunStats
();
}
...
...
@@ -441,6 +461,20 @@ namespace Tools.MyAgent
catch
{
return
0
;
}
}
/// <summary>gauge:当日观测文件(runs/metrics jsonl)总字节数,供观测自查。</summary>
private
static
long
LogsSizeToday
()
{
try
{
long
total
=
0
;
string
today
=
DateTime
.
Now
.
ToString
(
"yyyyMMdd"
);
foreach
(
var
f
in
Directory
.
GetFiles
(
LogsRoot
,
"*"
+
today
+
"*.jsonl"
))
total
+=
new
FileInfo
(
f
).
Length
;
return
total
;
}
catch
{
return
0
;
}
}
/// <summary>按前缀汇总(用于无维度的总量)。</summary>
public
static
long
SumPrefix
(
string
prefix
)
{
...
...
@@ -643,7 +677,7 @@ namespace Tools.MyAgent
catch
{
}
}
/// <summary>落一条指标快照(计数器 + 直方图读数)。</summary>
/// <summary>落一条指标快照(计数器 + 直方图读数
;直方图带原始桶分布,供重启重放精确合并
)。</summary>
public
static
void
FlushMetrics
()
{
try
...
...
@@ -654,8 +688,8 @@ namespace Tools.MyAgent
{
[
"ts"
]
=
DateTime
.
UtcNow
.
ToString
(
"o"
),
[
"counters"
]
=
new
Dictionary
<
string
,
long
>(
Counters
),
[
"run_ms"
]
=
Hist
ogram
(
"agent_run_duration_ms"
),
[
"ttfb_ms"
]
=
Hist
ogram
(
"agent_ttfb_ms"
),
[
"run_ms"
]
=
Hist
Snapshot
(
"agent_run_duration_ms"
),
[
"ttfb_ms"
]
=
Hist
Snapshot
(
"agent_ttfb_ms"
),
};
string
line
=
JsonConvert
.
SerializeObject
(
snap
);
var
w
=
GetMetricWriter
();
...
...
@@ -664,6 +698,96 @@ namespace Tools.MyAgent
catch
{
}
}
/// <summary>单个直方图的落盘形态:count/sum/min/max + 12 桶分布(P1-6 重放合并的数据源)。</summary>
private
static
object
HistSnapshot
(
string
key
)
{
try
{
if
(!
Histograms
.
TryGetValue
(
key
,
out
var
h
))
return
new
HistogramView
();
lock
(
h
)
{
return
new
{
count
=
h
.
Total
,
sum
=
h
.
Sum
,
min
=
h
.
Min
==
long
.
MaxValue
?
0
:
h
.
Min
,
max
=
h
.
Max
,
buckets
=
h
.
Counts
,
};
}
}
catch
{
return
new
HistogramView
();
}
}
/// <summary>
/// P1-6:启动时重放当日 metrics_*.jsonl 最后一条快照并合并进内存,
/// 使 <c>/stats</c> 与设置页 KPI 在进程重启后不丢当日累计(看板读 jsonl 本就不受影响)。
/// <para>合并语义:计数器 = 存量 + 当前(重放只在启动时做一次,此刻当前≈0);
/// 直方图按桶分布相加。旧格式快照(无 buckets)跳过直方图部分。</para>
/// </summary>
private
static
bool
_replayed
;
private
static
void
ReplayTodayMetrics
()
{
try
{
if
(
_replayed
)
return
;
_replayed
=
true
;
string
path
=
Path
.
Combine
(
LogsRoot
,
"metrics_"
+
DateTime
.
Now
.
ToString
(
"yyyyMMdd"
)
+
".jsonl"
);
if
(!
File
.
Exists
(
path
))
return
;
string
last
=
null
;
using
(
var
fs
=
new
FileStream
(
path
,
FileMode
.
Open
,
FileAccess
.
Read
,
FileShare
.
ReadWrite
))
using
(
var
sr
=
new
StreamReader
(
fs
))
{
string
l
;
while
((
l
=
sr
.
ReadLine
())
!=
null
)
if
(!
string
.
IsNullOrWhiteSpace
(
l
))
last
=
l
;
// 取最后一条完整快照
}
if
(
string
.
IsNullOrEmpty
(
last
))
return
;
var
o
=
JObject
.
Parse
(
last
);
if
(
o
[
"counters"
]
is
JObject
counters
)
foreach
(
var
kv
in
counters
)
{
if
(
kv
.
Value
==
null
||
!
kv
.
Value
.
Type
.
ToString
().
StartsWith
(
"Integer"
))
continue
;
long
stored
=
(
long
)
kv
.
Value
;
if
(
stored
<=
0
)
continue
;
Counters
.
AddOrUpdate
(
kv
.
Key
,
stored
,
(
_
,
cur
)
=>
cur
+
stored
);
}
MergeHist
(
o
[
"run_ms"
],
"agent_run_duration_ms"
);
MergeHist
(
o
[
"ttfb_ms"
],
"agent_ttfb_ms"
);
}
catch
{
}
}
/// <summary>把一条落盘直方图快照按桶合并进内存(count/sum/min/max + buckets)。</summary>
private
static
void
MergeHist
(
JToken
snap
,
string
key
)
{
try
{
if
(!(
snap
is
JObject
jo
))
return
;
var
jBuckets
=
jo
[
"buckets"
]
as
JArray
;
if
(
jBuckets
==
null
)
return
;
// 旧格式无桶分布,跳过(避免破坏分位数)
long
count
=
(
long
?)
jo
[
"count"
]
??
0
;
if
(
count
<=
0
)
return
;
long
sum
=
(
long
?)
jo
[
"sum"
]
??
0
;
long
min
=
(
long
?)
jo
[
"min"
]
??
0
;
long
max
=
(
long
?)
jo
[
"max"
]
??
0
;
var
h
=
Histograms
.
GetOrAdd
(
key
,
_
=>
new
Hist
());
lock
(
h
)
{
for
(
int
i
=
0
;
i
<
Buckets
.
Length
&&
i
<
jBuckets
.
Count
;
i
++)
h
.
Counts
[
i
]
+=
(
long
)
jBuckets
[
i
];
h
.
Total
+=
count
;
h
.
Sum
+=
sum
;
if
(
min
<
h
.
Min
)
h
.
Min
=
min
;
if
(
max
>
h
.
Max
)
h
.
Max
=
max
;
}
}
catch
{
}
}
/// <summary>把队列与缓冲落盘(进程退出 / 手动调用)。</summary>
public
static
void
Flush
()
{
...
...
Tools.Agent/wMyAgent.cs
View file @
12f2ed0d
...
...
@@ -161,6 +161,7 @@ namespace Tools.MyAgent
_engine
=
new
AgentEngine
(
_kb
,
_agentRoot
,
_productContext
,
diagnosticRoots
);
LoadConfig
();
ApplyConfig
();
RunStartupHealthCheck
();
// HealthCheckOnStart=true 时后台自检落 health.json(P1-4:原为死配置)
WireEvents
();
RefreshChat
();
UpdateStatusIndicator
();
...
...
@@ -433,6 +434,20 @@ namespace Tools.MyAgent
SaveAgentConfig
(
_config
);
}
/// <summary>P1-4:HealthCheckOnStart=true 时启动后台被动自检并落 health.json(不做模型服务网络探测,避免拖慢启动)。</summary>
private
void
RunStartupHealthCheck
()
{
if
(!
_config
.
HealthCheckOnStart
)
return
;
var
cfg
=
_config
;
var
kb
=
_kb
;
var
roots
=
_engine
!=
null
?
_engine
.
DiagnosticRoots
:
null
;
System
.
Threading
.
Tasks
.
Task
.
Run
(()
=>
{
try
{
AgentHealth
.
Save
(
AgentHealth
.
Check
(
cfg
,
kb
,
roots
));
}
catch
{
}
});
}
private
void
ApplyConfig
()
{
if
(
_productContext
!=
null
)
...
...
@@ -2812,7 +2827,7 @@ namespace Tools.MyAgent
try
{
int
days
=
_config
.
DashboardDays
>
0
&&
_config
.
DashboardDays
<=
30
?
_config
.
DashboardDays
:
7
;
var
form
=
new
Tools
.
MyAgent
.
Forms
.
AgentAdminForm
(
null
,
days
,
AgentTelemetry
.
IsDetailed
);
var
form
=
new
Tools
.
MyAgent
.
Forms
.
AgentAdminForm
(
null
,
days
,
AgentTelemetry
.
IsDetailed
,
_config
.
AdminAutoRefresh
);
form
.
Show
(
this
);
}
catch
(
Exception
ex
)
...
...
@@ -2850,6 +2865,8 @@ namespace Tools.MyAgent
if
(
tokens
>
0
)
sb
.
Append
(
"- Token:"
).
Append
(
st
.
PromptTokens
).
Append
(
" + "
).
Append
(
st
.
CompletionTokens
).
Append
(
"\n"
);
if
(
st
.
Cost
>
0
)
sb
.
Append
(
"- 费用:"
).
Append
(
CostEstimator
.
Format
(
st
.
Cost
)).
Append
(
"\n"
);
if
(
st
.
Dropped
>
0
)
sb
.
Append
(
"- ⚠ 丢弃 "
).
Append
(
st
.
Dropped
).
Append
(
" 条(采集跟不上)\n"
);
sb
.
Append
(
"- 观测自查:待落盘 "
).
Append
(
st
.
QueueDepth
).
Append
(
" 条 · 当日 jsonl "
)
.
Append
(
st
.
LogSizeToday
/
1024
).
Append
(
" KB\n"
);
if
(
st
.
Runs
==
0
)
sb
.
Append
(
"_暂无数据(本进程尚未完成任何一轮回答)_\n"
);
AddSystemMessage
(
sb
.
ToString
());
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment