feat: Phase4 命令系统与上下文增强
新增基础设施: - TokenTracker: 真实Token使用量追踪和费用估算(按模型定价) - SkillLoader: .claude/skills/ 扫描加载,支持YAML frontmatter元数据 - GitContext: Git分支/状态/最近提交收集,注入系统提示词 新增命令 (4个): - /init: 项目CLAUDE.md初始化向导,自动检测项目类型(Maven/Gradle/Node/Python等) - /status: 会话状态仪表板(模型、Token、内存、运行时间等) - /context: 上下文概览(CLAUDE.md、Skills、Git、系统提示词大小) - /config: 配置查看/设置(支持model快捷切换和API key显示) 增强命令 (3个): - /cost: 从占位→真实Token统计,显示input/output/cache tokens和费用 - /model: 支持快捷名称切换(sonnet/opus/haiku),显示可用模型列表 - /compact: 增强显示压缩前后消息数和Token使用量 系统提示词增强: - 集成Skills摘要和Git上下文到系统提示词 - 新增Shell环境信息和更详细的Guidelines 命令总数: 6 → 10, 工具总数: 11 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -41,6 +41,7 @@ public class AgentLoop {
|
||||
private final ToolRegistry toolRegistry;
|
||||
private final ToolContext toolContext;
|
||||
private final String systemPrompt;
|
||||
private final TokenTracker tokenTracker;
|
||||
|
||||
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
|
||||
private final List<Message> messageHistory = new ArrayList<>();
|
||||
@@ -53,10 +54,16 @@ public class AgentLoop {
|
||||
|
||||
public AgentLoop(ChatModel chatModel, ToolRegistry toolRegistry,
|
||||
ToolContext toolContext, String systemPrompt) {
|
||||
this(chatModel, toolRegistry, toolContext, systemPrompt, new TokenTracker());
|
||||
}
|
||||
|
||||
public AgentLoop(ChatModel chatModel, ToolRegistry toolRegistry,
|
||||
ToolContext toolContext, String systemPrompt, TokenTracker tokenTracker) {
|
||||
this.chatModel = chatModel;
|
||||
this.toolRegistry = toolRegistry;
|
||||
this.toolContext = toolContext;
|
||||
this.systemPrompt = systemPrompt;
|
||||
this.tokenTracker = tokenTracker;
|
||||
// 添加系统提示词到消息历史
|
||||
this.messageHistory.add(new SystemMessage(systemPrompt));
|
||||
}
|
||||
@@ -94,6 +101,15 @@ public class AgentLoop {
|
||||
Prompt prompt = new Prompt(List.copyOf(messageHistory), options);
|
||||
ChatResponse response = chatModel.call(prompt);
|
||||
|
||||
// 记录 Token 使用量
|
||||
if (response.getMetadata() != null && response.getMetadata().getUsage() != null) {
|
||||
var usage = response.getMetadata().getUsage();
|
||||
tokenTracker.recordUsage(
|
||||
usage.getPromptTokens(),
|
||||
usage.getCompletionTokens()
|
||||
);
|
||||
}
|
||||
|
||||
AssistantMessage assistant = response.getResult().getOutput();
|
||||
messageHistory.add(assistant);
|
||||
|
||||
@@ -169,6 +185,16 @@ public class AgentLoop {
|
||||
return Collections.unmodifiableList(messageHistory);
|
||||
}
|
||||
|
||||
/** 获取 Token 追踪器 */
|
||||
public TokenTracker getTokenTracker() {
|
||||
return tokenTracker;
|
||||
}
|
||||
|
||||
/** 获取系统提示词 */
|
||||
public String getSystemPrompt() {
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
/** 重置历史(保留系统提示词) */
|
||||
public void reset() {
|
||||
messageHistory.clear();
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.claudecode.core;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Token 使用量追踪器 —— 记录 API 调用的 token 消耗。
|
||||
* <p>
|
||||
* 从 ChatResponse 的 usage 元数据中提取 token 统计信息,
|
||||
* 支持按会话累计和费用估算。
|
||||
*/
|
||||
public class TokenTracker {
|
||||
|
||||
private final AtomicLong totalInputTokens = new AtomicLong(0);
|
||||
private final AtomicLong totalOutputTokens = new AtomicLong(0);
|
||||
private final AtomicLong totalCacheReadTokens = new AtomicLong(0);
|
||||
private final AtomicLong totalCacheCreationTokens = new AtomicLong(0);
|
||||
private final AtomicLong apiCallCount = new AtomicLong(0);
|
||||
|
||||
/** 模型定价(每百万 token 的美元价格) */
|
||||
private double inputPricePerMillion = 3.0; // Claude Sonnet 4 input
|
||||
private double outputPricePerMillion = 15.0; // Claude Sonnet 4 output
|
||||
private double cacheReadPricePerMillion = 0.3; // 缓存读取
|
||||
private String modelName = "claude-sonnet-4-20250514";
|
||||
|
||||
/** 记录一次 API 调用的 token 使用 */
|
||||
public void recordUsage(long inputTokens, long outputTokens) {
|
||||
totalInputTokens.addAndGet(inputTokens);
|
||||
totalOutputTokens.addAndGet(outputTokens);
|
||||
apiCallCount.incrementAndGet();
|
||||
}
|
||||
|
||||
/** 记录一次包含缓存的 API 调用 */
|
||||
public void recordUsage(long inputTokens, long outputTokens, long cacheRead, long cacheCreation) {
|
||||
totalInputTokens.addAndGet(inputTokens);
|
||||
totalOutputTokens.addAndGet(outputTokens);
|
||||
totalCacheReadTokens.addAndGet(cacheRead);
|
||||
totalCacheCreationTokens.addAndGet(cacheCreation);
|
||||
apiCallCount.incrementAndGet();
|
||||
}
|
||||
|
||||
/** 设置模型和对应定价 */
|
||||
public void setModel(String model) {
|
||||
this.modelName = model;
|
||||
// 根据模型设置定价
|
||||
if (model.contains("opus")) {
|
||||
inputPricePerMillion = 15.0;
|
||||
outputPricePerMillion = 75.0;
|
||||
cacheReadPricePerMillion = 1.5;
|
||||
} else if (model.contains("sonnet")) {
|
||||
inputPricePerMillion = 3.0;
|
||||
outputPricePerMillion = 15.0;
|
||||
cacheReadPricePerMillion = 0.3;
|
||||
} else if (model.contains("haiku")) {
|
||||
inputPricePerMillion = 0.25;
|
||||
outputPricePerMillion = 1.25;
|
||||
cacheReadPricePerMillion = 0.03;
|
||||
}
|
||||
}
|
||||
|
||||
public long getInputTokens() { return totalInputTokens.get(); }
|
||||
public long getOutputTokens() { return totalOutputTokens.get(); }
|
||||
public long getCacheReadTokens() { return totalCacheReadTokens.get(); }
|
||||
public long getCacheCreationTokens() { return totalCacheCreationTokens.get(); }
|
||||
public long getTotalTokens() { return totalInputTokens.get() + totalOutputTokens.get(); }
|
||||
public long getApiCallCount() { return apiCallCount.get(); }
|
||||
public String getModelName() { return modelName; }
|
||||
|
||||
/** 估算当前会话费用(美元) */
|
||||
public double estimateCost() {
|
||||
double inputCost = totalInputTokens.get() * inputPricePerMillion / 1_000_000.0;
|
||||
double outputCost = totalOutputTokens.get() * outputPricePerMillion / 1_000_000.0;
|
||||
double cacheCost = totalCacheReadTokens.get() * cacheReadPricePerMillion / 1_000_000.0;
|
||||
return inputCost + outputCost + cacheCost;
|
||||
}
|
||||
|
||||
/** 重置统计 */
|
||||
public void reset() {
|
||||
totalInputTokens.set(0);
|
||||
totalOutputTokens.set(0);
|
||||
totalCacheReadTokens.set(0);
|
||||
totalCacheCreationTokens.set(0);
|
||||
apiCallCount.set(0);
|
||||
}
|
||||
|
||||
/** 格式化 token 数量(带千位分隔) */
|
||||
public static String formatTokens(long tokens) {
|
||||
if (tokens < 1000) return String.valueOf(tokens);
|
||||
if (tokens < 1_000_000) return String.format("%.1fK", tokens / 1000.0);
|
||||
return String.format("%.2fM", tokens / 1_000_000.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user