签到工具新增检查功能:修复状态误判问题
问题:
- 用户询问'我今天签到了吗'时,AI基于对话历史判断而非查询数据库
- 导致明明没签到,AI却说已经签到了(误判)
解决方案:
- 新增 action=check 操作,专门用于检查当前节点今天是否已签到
- 直接调用 HasSignedOnDay 查询数据库,返回准确的签到状态
- AI 现在会强制调用工具查询,而不是根据记忆猜测
改动内容:
- 工具定义中添加 check 操作类型
- 实现 executeCheck 方法,查询数据库并返回明确状态
- 添加完整的单元测试(未签到/已签到两种场景)
- 更新文档说明 check 操作的使用
使用示例:
- 用户:'我今天签到了吗?'
- AI 调用:{"action": "check"}
- 返回:'XXX 今天还没有签到。' 或 'XXX 今天已经签到过了。'
测试:
- ✅ 未签到场景测试通过
- ✅ 已签到场景测试通过
- ✅ 所有签到工具测试通过
- ✅ 项目编译成功
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,11 @@
|
|||||||
1. **数据不准确**:AI 只能根据上下文猜测或给出模糊答案
|
1. **数据不准确**:AI 只能根据上下文猜测或给出模糊答案
|
||||||
2. **无法查询历史**:询问超过一天的数据时,AI 一问三不知
|
2. **无法查询历史**:询问超过一天的数据时,AI 一问三不知
|
||||||
3. **功能不完整**:数据库层已有完善的查询能力(`CountSigns`、`CountSignsByDay`),但工具层未暴露给 AI
|
3. **功能不完整**:数据库层已有完善的查询能力(`CountSigns`、`CountSignsByDay`),但工具层未暴露给 AI
|
||||||
|
4. **状态判断不准**:用户问"我今天签到了吗"时,AI 基于对话记忆判断而非查询数据库,导致误判
|
||||||
|
|
||||||
## 解决方案
|
## 解决方案
|
||||||
|
|
||||||
为签到工具添加查询功能,支持以下两种操作模式:
|
为签到工具添加查询和检查功能,支持以下三种操作模式:
|
||||||
|
|
||||||
### 1. 签到操作 (action=sign)
|
### 1. 签到操作 (action=sign)
|
||||||
|
|
||||||
@@ -21,13 +22,20 @@
|
|||||||
|
|
||||||
### 2. 查询操作 (action=query)
|
### 2. 查询操作 (action=query)
|
||||||
|
|
||||||
新增查询功能:
|
查询签到统计功能:
|
||||||
- 查询指定日期或日期范围的签到统计
|
- 查询指定日期或日期范围的签到统计
|
||||||
- 返回总人次和按天分组的统计数据
|
- 返回总人次和按天分组的统计数据
|
||||||
- 可选参数:
|
- 可选参数:
|
||||||
- `date`: 查询日期(格式:YYYY-MM-DD),默认今天
|
- `date`: 查询日期(格式:YYYY-MM-DD),默认今天
|
||||||
- `days`: 查询最近 N 天,默认只查询 date 指定的那一天
|
- `days`: 查询最近 N 天,默认只查询 date 指定的那一天
|
||||||
|
|
||||||
|
### 3. 检查操作 (action=check) **新增**
|
||||||
|
|
||||||
|
检查当前节点今天是否已签到:
|
||||||
|
- 用于回答"我今天签到了吗"之类的问题
|
||||||
|
- 直接查询数据库,不依赖对话历史
|
||||||
|
- 返回明确的已签到/未签到状态
|
||||||
|
|
||||||
## 修改内容
|
## 修改内容
|
||||||
|
|
||||||
### 1. 扩展 SignStore 接口
|
### 1. 扩展 SignStore 接口
|
||||||
@@ -82,6 +90,27 @@ type signParams struct {
|
|||||||
|
|
||||||
### AI 调用示例
|
### AI 调用示例
|
||||||
|
|
||||||
|
#### 检查今天是否签到(check 操作)
|
||||||
|
|
||||||
|
**用户**:"我今天签到了吗?"
|
||||||
|
|
||||||
|
AI 调用:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "check"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回(未签到):
|
||||||
|
```
|
||||||
|
Test Node 今天还没有签到。
|
||||||
|
```
|
||||||
|
|
||||||
|
返回(已签到):
|
||||||
|
```
|
||||||
|
Test Node 今天已经签到过了。
|
||||||
|
```
|
||||||
|
|
||||||
#### 查询今天的签到情况
|
#### 查询今天的签到情况
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ func (t *Tool) Enabled() bool { return t.enabled && t.store != nil }
|
|||||||
|
|
||||||
// ToolDefinition returns the OpenAI tool definition
|
// ToolDefinition returns the OpenAI tool definition
|
||||||
func (t *Tool) ToolDefinition(description string) *model.Tool {
|
func (t *Tool) ToolDefinition(description string) *model.Tool {
|
||||||
desc := "签到工具。支持签到和查询两种操作:\n" +
|
desc := "签到工具。支持三种操作:\n" +
|
||||||
"1. 签到操作(action=sign):记录节点今日签到信息,每个节点每天只能签到一次。必填参数:地区、名字、设备\n" +
|
"1. 签到操作(action=sign):记录节点今日签到信息,每个节点每天只能签到一次。必填参数:地区、名字、设备\n" +
|
||||||
"2. 查询操作(action=query):查询签到统计。可按日期范围查询,默认查询今天。返回签到总数和按天的统计数据"
|
"2. 查询操作(action=query):查询签到统计。可按日期范围查询,默认查询今天。返回签到总数和按天的统计数据\n" +
|
||||||
|
"3. 检查操作(action=check):检查当前节点今天是否已签到。用于回答用户'我今天签到了吗'之类的问题"
|
||||||
if description != "" {
|
if description != "" {
|
||||||
desc = description
|
desc = description
|
||||||
}
|
}
|
||||||
@@ -61,8 +62,8 @@ func (t *Tool) ToolDefinition(description string) *model.Tool {
|
|||||||
"properties": map[string]any{
|
"properties": map[string]any{
|
||||||
"action": map[string]any{
|
"action": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": []string{"sign", "query"},
|
"enum": []string{"sign", "query", "check"},
|
||||||
"description": "操作类型:sign=签到,query=查询签到统计",
|
"description": "操作类型:sign=签到,query=查询签到统计,check=检查今天是否已签到",
|
||||||
},
|
},
|
||||||
"region": map[string]any{
|
"region": map[string]any{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -122,10 +123,12 @@ func (t *Tool) Execute(ctx context.Context, args string, runtime agenttool.Runti
|
|||||||
switch strings.ToLower(strings.TrimSpace(params.Action)) {
|
switch strings.ToLower(strings.TrimSpace(params.Action)) {
|
||||||
case "query":
|
case "query":
|
||||||
return t.executeQuery(ctx, params, runtime)
|
return t.executeQuery(ctx, params, runtime)
|
||||||
|
case "check":
|
||||||
|
return t.executeCheck(ctx, params, runtime)
|
||||||
case "sign", "":
|
case "sign", "":
|
||||||
return t.executeSign(ctx, params, runtime)
|
return t.executeSign(ctx, params, runtime)
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("无效的操作类型:%s,只支持 sign 或 query", params.Action)
|
return "", fmt.Errorf("无效的操作类型:%s,只支持 sign、query 或 check", params.Action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +179,31 @@ func (t *Tool) executeSign(ctx context.Context, params signParams, runtime agent
|
|||||||
return fmt.Sprintf("签到成功!%s\n签到内容:%s", displayName(node), record.SignText), nil
|
return fmt.Sprintf("签到成功!%s\n签到内容:%s", displayName(node), record.SignText), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// executeCheck 检查当前节点今天是否已签到
|
||||||
|
func (t *Tool) executeCheck(ctx context.Context, params signParams, runtime agenttool.Runtime) (string, error) {
|
||||||
|
// 节点身份来自消息上下文
|
||||||
|
node, ok := agenttool.NodeContextFromContext(ctx)
|
||||||
|
if !ok || strings.TrimSpace(node.NodeID) == "" {
|
||||||
|
return "", fmt.Errorf("缺少发送节点上下文,无法检查签到状态")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := runtime.Now
|
||||||
|
if now.IsZero() {
|
||||||
|
now = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据库检查今天是否已签到
|
||||||
|
signed, err := t.store.HasSignedOnDay(node.NodeID, now)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("检查签到状态失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if signed {
|
||||||
|
return fmt.Sprintf("%s 今天已经签到过了。", displayName(node)), nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s 今天还没有签到。", displayName(node)), nil
|
||||||
|
}
|
||||||
|
|
||||||
// executeQuery 执行查询操作
|
// executeQuery 执行查询操作
|
||||||
func (t *Tool) executeQuery(ctx context.Context, params signParams, runtime agenttool.Runtime) (string, error) {
|
func (t *Tool) executeQuery(ctx context.Context, params signParams, runtime agenttool.Runtime) (string, error) {
|
||||||
now := runtime.Now
|
now := runtime.Now
|
||||||
|
|||||||
@@ -218,3 +218,100 @@ func TestSignTool_SignAction(t *testing.T) {
|
|||||||
t.Errorf("Expected 1 sign record, got %d", len(store.signs))
|
t.Errorf("Expected 1 sign record, got %d", len(store.signs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSignTool_CheckAction(t *testing.T) {
|
||||||
|
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// 测试场景1:今天未签到
|
||||||
|
t.Run("今天未签到", func(t *testing.T) {
|
||||||
|
store := &mockSignStore{
|
||||||
|
signs: []storepkg.SignRecord{},
|
||||||
|
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
|
||||||
|
}
|
||||||
|
|
||||||
|
tool := &Tool{
|
||||||
|
enabled: true,
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := signParams{
|
||||||
|
Action: "check",
|
||||||
|
}
|
||||||
|
argsJSON, _ := json.Marshal(params)
|
||||||
|
|
||||||
|
nodeCtx := agenttool.NodeContext{
|
||||||
|
NodeID: "test_node_123",
|
||||||
|
LongName: "Test Node",
|
||||||
|
ShortName: "TN",
|
||||||
|
}
|
||||||
|
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
|
||||||
|
|
||||||
|
runtime := agenttool.Runtime{Now: now}
|
||||||
|
result, err := tool.Execute(ctx, string(argsJSON), runtime)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Check result (not signed): %s", result)
|
||||||
|
|
||||||
|
if !contains(result, "还没有签到") && !contains(result, "没有签到") {
|
||||||
|
t.Errorf("Expected result to indicate not signed yet")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试场景2:今天已签到
|
||||||
|
t.Run("今天已签到", func(t *testing.T) {
|
||||||
|
store := &mockSignStore{
|
||||||
|
signs: []storepkg.SignRecord{
|
||||||
|
{
|
||||||
|
NodeID: "test_node_123",
|
||||||
|
SignText: "上海-TestUser-TestDevice签到",
|
||||||
|
SignTime: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
|
||||||
|
}
|
||||||
|
|
||||||
|
tool := &Tool{
|
||||||
|
enabled: true,
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := signParams{
|
||||||
|
Action: "check",
|
||||||
|
}
|
||||||
|
argsJSON, _ := json.Marshal(params)
|
||||||
|
|
||||||
|
nodeCtx := agenttool.NodeContext{
|
||||||
|
NodeID: "test_node_123",
|
||||||
|
LongName: "Test Node",
|
||||||
|
ShortName: "TN",
|
||||||
|
}
|
||||||
|
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
|
||||||
|
|
||||||
|
runtime := agenttool.Runtime{Now: now}
|
||||||
|
result, err := tool.Execute(ctx, string(argsJSON), runtime)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("Check result (already signed): %s", result)
|
||||||
|
|
||||||
|
if !contains(result, "已经签到") {
|
||||||
|
t.Errorf("Expected result to indicate already signed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) >= len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsMiddle(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user