基本完成

This commit is contained in:
2026-06-20 02:15:15 +08:00
parent fd766be731
commit 54df706ca7
7 changed files with 415 additions and 14 deletions
+87 -3
View File
@@ -2,6 +2,7 @@ package toolrouter
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
@@ -107,6 +108,13 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, s
}
// toolUsed 记录本轮是否真的执行了至少一次工具调用,供调用方决定是否跳过话题选择等后续门控。
toolUsed := false
// 签到意图强制调用:用户明确想签到(「签到/打卡/上台」等)时,模型却不主动调
// sign 工具的话,会被下游话题判定当成噪音丢弃。这里在循环外预判意图,待模型
// 该轮未请求任何工具时强制注入一次 sign 调用(用用户原文作为 raw_text),
// 保证签到一定落库、且不会被话题判定丢弃。
forceSignText := detectSignIntent(chatMessages)
_, signAvailable := toolByName["sign"]
signInvoked := false // 本轮循环中是否已经调用过 sign(含模型主动调与强制调)
for i := 0; i < maxAgentToolIterations; i++ {
if emit != nil {
emit(stream.Frame{Type: "trace", Tool: "agent_tools", Stage: "request", Status: "running", Message: fmt.Sprintf("正在进行第 %d 轮工具判断", i+1), Data: map[string]any{"iteration": i + 1, "max_iterations": maxAgentToolIterations, "tools": availableNames}})
@@ -137,10 +145,19 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, s
calls = []*model.ToolCall{{ID: "legacy_function_call", Type: model.ToolTypeFunction, Function: *choice.Message.FunctionCall}}
}
if len(calls) == 0 {
if emit != nil {
emit(stream.Frame{Type: "trace", Tool: "agent_tools", Stage: "request", Status: "success", Message: "模型未请求工具,进入回答生成"})
// 模型本轮未请求任何工具。若检测到签到意图且 sign 工具可用、本次循环尚未调过 sign,
// 则强制注入一次 sign 调用(以用户原文作为 raw_text),保证签到一定落库。
if forced := buildForcedSignCall(forceSignText, signAvailable, signInvoked); forced != nil {
if emit != nil {
emit(stream.Frame{Type: "trace", Tool: "sign", Stage: "tool_calls", Status: "running", Message: "检测到签到意图,模型未调用签到工具,强制调用 sign", Data: map[string]any{"tools": []string{"sign"}, "forced": true, "iteration": i + 1}})
}
calls = []*model.ToolCall{forced}
} else {
if emit != nil {
emit(stream.Frame{Type: "trace", Tool: "agent_tools", Stage: "request", Status: "success", Message: "模型未请求工具,进入回答生成"})
}
return finalMessages, toolUsed, nil
}
return finalMessages, toolUsed, nil
}
callNames := make([]string, 0, len(calls))
for _, call := range calls {
@@ -157,6 +174,9 @@ func RunAgentToolLoop(ctx context.Context, state *State, profile *llm.Profile, s
finalMessages = append(finalMessages, assistantMessage)
decisionMessages = append(decisionMessages, assistantMessage)
for _, call := range calls {
if call != nil && call.Function.Name == "sign" {
signInvoked = true
}
result := ExecuteAgentToolCall(ctx, call, toolByName, emit)
resultContent := &model.ChatCompletionMessageContent{StringValue: &result}
toolMessage := &model.ChatCompletionMessage{Role: "tool", ToolCallID: call.ID, Content: resultContent}
@@ -195,3 +215,67 @@ func BoolPtr(b bool) *bool {
func IntPtr(i int) *int {
return &i
}
// signIntentKeywords 是判定签到意图的关键词。命中任一即认为用户想签到。
var signIntentKeywords = []string{"签到", "打卡", "上台"}
// detectSignIntent 取最后一条用户消息,若包含签到意图关键词则返回该消息原文
//(去除前缀的「[来自 ...]」等格式化包装),否则返回空串。
func detectSignIntent(chatMessages []message.ChatMessage) string {
userText := lastUserMessageText(chatMessages)
if strings.TrimSpace(userText) == "" {
return ""
}
for _, kw := range signIntentKeywords {
if strings.Contains(userText, kw) {
return stripFromPrefix(userText)
}
}
return ""
}
// stripFromPrefix 去掉 autoreply.formatUserMessage 加上的「[来自 ...] 」前缀,
// 让签到正文只保留用户实际发送的内容。
func stripFromPrefix(s string) string {
if idx := strings.Index(s, "]"); idx >= 0 && strings.HasPrefix(strings.TrimSpace(s), "[") {
return strings.TrimSpace(s[idx+1:])
}
return s
}
// buildForcedSignCall 在满足条件时构造一次强制 sign 调用。条件:
// - 有签到意图原文(signText 非空)
// - sign 工具可用
// - 本次循环尚未调过 sign(避免重复签到)
//
// 调用参数仅含 raw_text(用户原文),由 sign 工具回退为签到正文。
func buildForcedSignCall(signText string, signAvailable, signInvoked bool) *model.ToolCall {
if strings.TrimSpace(signText) == "" || !signAvailable || signInvoked {
return nil
}
args, _ := json.Marshal(map[string]string{"raw_text": signText})
argsStr := string(args)
return &model.ToolCall{
ID: "forced_sign",
Type: model.ToolTypeFunction,
Function: model.FunctionCall{
Name: "sign",
Arguments: argsStr,
},
}
}
// lastUserMessageText 返回消息列表中最后一条 role 为 user 的消息内容。
func lastUserMessageText(messages []message.ChatMessage) string {
for i := len(messages) - 1; i >= 0; i-- {
msg := messages[i]
role := msg.Role
if role == "" {
role = "user"
}
if role == "user" {
return msg.Content
}
}
return ""
}