mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
feat: 完整实现 NLP 三元组提取系统 + token budget 上下文分配
- 重写 extractor.go: 分句、17条 POS 模板、依存模板 + COO 链、ATT合并 - parser.go: 分句循环 + TransE 向量验证(h+r≈t) - fallback.go: jieba POS 降级解析器 - bridge.go: nlp.Triple ↔ memory.Triple 转换 - pipeline.go: extractKeyTriples 改用 NLP 提取器, 删除5条旧前缀规则 - distill.go: docToTriples 改用 NLP 提取器 - reorgGraph: 语义相似度增强检测, 保持纯 LLM 决断 - Provider 接口加 MaxContextTokens() + 模型窗口映射表 - tokenbudget.go: 中文 token 估算器 + budget 分配(80%利用率) - process.go/buildSystemPrompt: 按 token 预算截断 memory+timeline
This commit is contained in:
@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/nlp"
|
||||
)
|
||||
|
||||
type RawRecord struct {
|
||||
@ -265,182 +266,24 @@ func (d *Distiller) cleanupRawFiles() {
|
||||
func extractKeyTriples(userContent, assistantContent string) []memory.Triple {
|
||||
var triples []memory.Triple
|
||||
|
||||
// 提取对话中的关键信息,而不是直接 dump 原文
|
||||
// 规则1: "我的名字是X" / "我叫X" → (用户, 姓名, X)
|
||||
if name := extractName(userContent); name != "" {
|
||||
triples = append(triples, memory.Triple{Subject: "用户", Relation: "姓名", Object: name})
|
||||
e := nlp.NewExtractor(nil)
|
||||
text := userContent
|
||||
if assistantContent != "" {
|
||||
text += assistantContent
|
||||
}
|
||||
// 规则2: "我住在X" / "我家在X" → (用户, 居住地, X)
|
||||
if loc := extractLocation(userContent); loc != "" {
|
||||
triples = append(triples, memory.Triple{Subject: "用户", Relation: "居住地", Object: loc})
|
||||
}
|
||||
// 规则3: "我喜欢X" / "我爱X" → (用户, 喜好, X)
|
||||
if like := extractLike(userContent); like != "" {
|
||||
triples = append(triples, memory.Triple{Subject: "用户", Relation: "喜好", Object: like})
|
||||
}
|
||||
// 规则4: "我X岁" / "我的年龄是X" → (用户, 年龄, X)
|
||||
if age := extractAge(userContent); age != "" {
|
||||
triples = append(triples, memory.Triple{Subject: "用户", Relation: "年龄", Object: age})
|
||||
}
|
||||
// 规则5: "我的工作是X" / "我在X工作" → (用户, 职业, X)
|
||||
if job := extractJob(userContent); job != "" {
|
||||
triples = append(triples, memory.Triple{Subject: "用户", Relation: "职业", Object: job})
|
||||
result := e.Extract(text)
|
||||
if result != nil {
|
||||
for _, nt := range result.Triples {
|
||||
mt := nlp.ToMemoryTriple(nt)
|
||||
if mt.Subject != "" && mt.Relation != "" && mt.Object != "" {
|
||||
triples = append(triples, mt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return triples
|
||||
}
|
||||
|
||||
func extractName(s string) string {
|
||||
patterns := []struct {
|
||||
prefix string
|
||||
suffix string
|
||||
}{
|
||||
{"我叫", ""},
|
||||
{"我的名字是", ""},
|
||||
{"名字是", ""},
|
||||
{"我是", ""},
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
for _, p := range patterns {
|
||||
if strings.HasPrefix(s, p.prefix) {
|
||||
candidate := strings.TrimPrefix(s, p.prefix)
|
||||
if p.suffix != "" && strings.Contains(candidate, p.suffix) {
|
||||
candidate = candidate[:strings.Index(candidate, p.suffix)]
|
||||
}
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
// 取第一个空格/逗号/句号前的内容
|
||||
for _, sep := range []string{",", "。", " ", ","} {
|
||||
if idx := strings.Index(candidate, sep); idx > 0 {
|
||||
candidate = candidate[:idx]
|
||||
}
|
||||
}
|
||||
// "我是张三"(姓名) vs "我是一个程序员"(职业):名字通常 ≤4 字符
|
||||
if p.prefix == "我是" && len([]rune(candidate)) > 4 {
|
||||
continue
|
||||
}
|
||||
if len(candidate) > 0 && len(candidate) < 20 {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractLocation(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
after := ""
|
||||
switch {
|
||||
case strings.HasPrefix(s, "我住在"):
|
||||
after = strings.TrimPrefix(s, "我住在")
|
||||
case strings.HasPrefix(s, "我家在"):
|
||||
after = strings.TrimPrefix(s, "我家在")
|
||||
case strings.HasPrefix(s, "我居住在"):
|
||||
after = strings.TrimPrefix(s, "我居住在")
|
||||
case strings.HasPrefix(s, "住在"):
|
||||
after = strings.TrimPrefix(s, "住在")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
for _, sep := range []string{"。", ",", " ", ","} {
|
||||
if idx := strings.Index(after, sep); idx > 0 {
|
||||
after = after[:idx]
|
||||
}
|
||||
}
|
||||
if len(after) > 0 && len(after) < 50 {
|
||||
return strings.TrimSpace(after)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractLike(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
after := ""
|
||||
switch {
|
||||
case strings.HasPrefix(s, "我喜欢"):
|
||||
after = strings.TrimPrefix(s, "我喜欢")
|
||||
case strings.HasPrefix(s, "我爱"):
|
||||
after = strings.TrimPrefix(s, "我爱")
|
||||
case strings.HasPrefix(s, "我最喜欢"):
|
||||
after = strings.TrimPrefix(s, "我最喜欢")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
for _, sep := range []string{"。", ",", " ", ","} {
|
||||
if idx := strings.Index(after, sep); idx > 0 {
|
||||
after = after[:idx]
|
||||
}
|
||||
}
|
||||
if len(after) > 0 && len(after) < 50 {
|
||||
return strings.TrimSpace(after)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractAge(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
after := ""
|
||||
switch {
|
||||
case strings.HasPrefix(s, "我"):
|
||||
rest := strings.TrimPrefix(s, "我")
|
||||
if strings.Contains(rest, "岁") {
|
||||
after = rest[:strings.Index(rest, "岁")]
|
||||
} else if strings.HasPrefix(rest, "的年龄是") {
|
||||
after = strings.TrimPrefix(rest, "的年龄是")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
for _, sep := range []string{"。", ",", " ", ","} {
|
||||
if idx := strings.Index(after, sep); idx > 0 {
|
||||
after = after[:idx]
|
||||
}
|
||||
}
|
||||
if len(after) > 0 && len(after) < 5 {
|
||||
return strings.TrimSpace(after)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractJob(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
after := ""
|
||||
switch {
|
||||
case strings.HasPrefix(s, "我的工作是"):
|
||||
after = strings.TrimPrefix(s, "我的工作是")
|
||||
case strings.HasPrefix(s, "我在"):
|
||||
rest := strings.TrimPrefix(s, "我在")
|
||||
if strings.Contains(rest, "工作") {
|
||||
after = rest[:strings.Index(rest, "工作")]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case strings.HasPrefix(s, "我是"):
|
||||
rest := strings.TrimPrefix(s, "我是")
|
||||
// "我是一个程序员" / "我是老师"
|
||||
for _, keyword := range []string{"一个", "一名", "一位"} {
|
||||
if strings.HasPrefix(rest, keyword) {
|
||||
rest = strings.TrimPrefix(rest, keyword)
|
||||
break
|
||||
}
|
||||
}
|
||||
// 职业通常较短,先看看
|
||||
after = rest
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
for _, sep := range []string{"。", ",", " ", ",", "。"} {
|
||||
if idx := strings.Index(after, sep); idx > 0 {
|
||||
after = after[:idx]
|
||||
}
|
||||
}
|
||||
if len(after) > 0 && len(after) < 20 {
|
||||
return strings.TrimSpace(after)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) > max {
|
||||
return s[:max] + "..."
|
||||
|
||||
Reference in New Issue
Block a user