feat(cloud-agent): 上下文计量 - 每步发出 context_pressure 读数并用上游实测定锚 (#601)

This commit is contained in:
AetherNo2332
2026-09-24 13:35:15 +08:00
committed by ddcat666
parent 7ca625730e
commit abf593c6c9
12 changed files with 1366 additions and 15 deletions

View File

@@ -157,6 +157,23 @@ type cloudAgentRuntime struct {
// 内存只保留最近一窗(repository.CloudAgentJournalWindow),因此它是"窗口在整条
// 日志里的偏移",而不是累计条数。只在内存里有效,不进检查点(检查点里的事件为空)。
EventSeqBase int `json:"-"`
// LastStep* 记下"最近一次已发出的模型调用"的本地计价,与上游回填的实测用量
// 配成锚点用。估算与实测指向同一份 canonical:估算取自任务 input 里实际发出的那份,
// 因此"信封一致"是构造保证,不需要额外比对。
LastStepTaskID string `json:"lastStepTaskId,omitempty"`
LastStepOperation string `json:"lastStepOperation,omitempty"`
LastStepEstimate int `json:"lastStepEstimate,omitempty"`
LastStepSourceBytes int `json:"lastStepSourceBytes,omitempty"`
LastStepSignature string `json:"lastStepSignature,omitempty"`
LastStepModel string `json:"lastStepModel,omitempty"`
LastStepChannelID string `json:"lastStepChannelId,omitempty"`
LastStepWindowTokens int `json:"lastStepWindowTokens,omitempty"`
// TokenAnchor 是上一步上游上报的用量(模型自己的分词器计数),上下文压力的权威锚点。
TokenAnchor *cloudAgentTokenAnchor `json:"tokenAnchor,omitempty"`
// ContextWindowKnown 记录本轮是否已经看到过"模型窗口已确认"的读数:从"未确认"变为
// "已确认"时要落一条 context_transition,消费方据此标"模型窗口已识别",
// 而不是把口径切换画成上下文骤降。
ContextWindowKnown bool `json:"contextWindowKnown,omitempty"`
}
type cloudAgentTransientReference struct {
@@ -170,6 +187,7 @@ type cloudAgentTransientReference struct {
func (s *Service) ensureCloudAgentExecution(task *model.Task, initial cloudAgentState) error {
var input struct {
TextHistory []providerTextMessage `json:"textHistory"`
Config map[string]any `json:"config"`
Requests struct {
Canonical canonicalAgentRequest `json:"canonical"`
} `json:"agentRequests"`
@@ -194,6 +212,27 @@ func (s *Service) ensureCloudAgentExecution(task *model.Task, initial cloudAgent
}
state.event(task.ID, "tool_completed", map[string]any{"toolName": "skills_load", "skillIds": skillIDs, "text": fmt.Sprintf("已启用 %d 个技能,正文将按需读取", len(initial.Skills))})
}
pressure := s.cloudAgentContextPressure(input.Requests.Canonical, initial.Request.Prompt, initial.Request)
// 第一步的模型调用就是根任务本身(不经过 enqueueCloudAgentTask):在这里登记任务 id
// 与本次请求的本地计价,它回来时才能与上游实测配成锚点。根任务的操作名是
// cloud_agent,但它就是第一步的模型调用,按"步骤"口径登记,否则回来配锚点时会被
// 操作名守卫挡掉。
state.LastStepTaskID = task.ID
state.LastStepOperation = cloudAgentStepOperation
state.LastStepEstimate = pressure.EstimatedInputTokens
state.LastStepSourceBytes = pressure.SourceBytes
state.LastStepModel = stringValue(input.Config["model"])
state.LastStepChannelID = stringValue(input.Config["channelId"])
state.LastStepSignature = cloudAgentRequestSignature(&state, input.Requests.Canonical, state.LastStepChannelID, state.LastStepModel)
if pressure.ModelLimitConfigured {
state.LastStepWindowTokens = pressure.ContextWindowTokens
}
// 第一步的窗口是"起始状态"而不是"刚刚识别":只播种标记,不落 window_resolved,
// 否则每轮开头都会报一次"模型窗口已识别"。
state.ContextWindowKnown = pressure.ModelLimitConfigured
firstPressure := cloudAgentContextPressurePayload(pressure, &state, input.Requests.Canonical)
firstPressure["requestId"] = task.ID
state.event(task.ID, "context_pressure", firstPressure)
run := &model.CloudAgentExecution{ID: task.ID, UserID: task.UserID, Status: "running", Revision: 1, CreatedAt: task.CreatedAt, UpdatedAt: time.Now()}
if err := cloudAgentSave(run, &state); err != nil {
return err
@@ -913,6 +952,9 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
return s.failCloudAgent(run, &state, fmt.Sprintf("达到 %d 次模型调用上限,本轮已停止", stepLimit))
}
cloudAgentDrainInterjections(run.ID, &state)
// 上一步的模型调用已经回来,先用它的上游实测用量更新压力锚点,再发下一步:
// 下一步的读数与后面的正文裁剪判定都要用到这份锚点。
s.recordCloudAgentTokenAnchor(run.UserID, &state)
// 轮内唯一裁剪 = 图片:超出保留轮次的看图结果换成文字回执(正文一律保留)。
// 它必须在压缩判定之前跑:图片是最贵的一类内容,先移出再评估 token 压力才有意义。
if changed, pruned := cloudAgentPruneInspectedImages(&state.Canonical, nil); changed {
@@ -1633,6 +1675,21 @@ func (s *Service) enqueueCloudAgentTask(run *model.CloudAgentExecution, state *c
return err
}
task.InputJSON = string(raw)
// 压力读数只对"模型调用"这一步有意义:媒体任务发的是另一份请求(另一套信封),
// 把读数挂到那份信封上会误导消费方。
var contextPressure *cloudAgentContextPressure
var requestCanonical canonicalAgentRequest
if media == nil {
if req.Operation != cloudAgentContextCompactionOperation {
canonical, ok := canonicalAgentRequestFromInput(input)
if !ok {
return fmt.Errorf("Agent 模型任务缺少可计量的请求信封")
}
requestCanonical = canonical
value := s.cloudAgentContextPressure(canonical, state.Request.Prompt, state.Request)
contextPressure = &value
}
}
if prepare.Order != nil {
task.BillingOrderID = prepare.Order.ID
}
@@ -1682,6 +1739,25 @@ func (s *Service) enqueueCloudAgentTask(run *model.CloudAgentExecution, state *c
if state.ContextCompaction != nil && req.Operation == cloudAgentContextCompactionOperation {
state.ContextCompaction.Status = "running"
} else {
if contextPressure != nil {
config, _ := input["config"].(map[string]any)
model, channelID := stringValue(config["model"]), stringValue(config["channelId"])
signature := cloudAgentRequestSignature(state, requestCanonical, channelID, model)
cloudAgentExpireTokenAnchorForRequest(run.ID, state, contextPressure.ContextWindowTokens, signature, model, channelID)
cloudAgentRecordMemorySegment(&state.Policy, requestCanonical.SystemPrompt)
payload := cloudAgentContextPressurePayload(*contextPressure, state, requestCanonical)
payload["requestId"] = task.ID
cloudAgentNoteContextWindowResolved(run.ID, state, *contextPressure)
state.event(run.ID, "context_pressure", payload)
state.LastStepTaskID = task.ID
state.LastStepOperation = req.Operation
state.LastStepEstimate = contextPressure.EstimatedInputTokens
state.LastStepSourceBytes = contextPressure.SourceBytes
state.LastStepSignature = signature
state.LastStepModel = model
state.LastStepChannelID = channelID
state.LastStepWindowTokens = contextPressure.ContextWindowTokens
}
state.Step++
}
}