Feed 把新 chunk 喂入解析器,返回 (textDelta, toolCalls)。 textDelta 是本轮新产生的可输出文本(不含 标签及其内部 JSON); toolCalls 是本轮闭合的 tool_call 列表,顺序与出现顺序一致。
(chunk string)
| 39 | // textDelta 是本轮新产生的可输出文本(不含 <tool_call> 标签及其内部 JSON); |
| 40 | // toolCalls 是本轮闭合的 tool_call 列表,顺序与出现顺序一致。 |
| 41 | func (p *Parser) Feed(chunk string) (textDelta string, toolCalls []official.ToolCall) { |
| 42 | p.buffer = normalize(p.buffer + chunk) |
| 43 | var text strings.Builder |
| 44 | for len(p.buffer) > 0 { |
| 45 | if !p.inside { |
| 46 | startIdx := strings.Index(p.buffer, StartTag) |
| 47 | if startIdx >= 0 { |
| 48 | pre := p.buffer[:startIdx] |
| 49 | if pre != "" { |
| 50 | // 已经被吐出的"半个标签"防误切,以及可能被截断的 JSON 防御 |
| 51 | // (looksLikeJSONJunk + 还没任何输出):这两种情况不输出,留待下个 chunk |
| 52 | if !(p.emittedCount == 0 && p.emittedText == false && looksLikeJSONJunk(pre)) { |
| 53 | text.WriteString(pre) |
| 54 | if strings.TrimSpace(pre) != "" { |
| 55 | p.emittedText = true |
| 56 | } |
| 57 | } |
| 58 | } |
| 59 | p.inside = true |
| 60 | p.buffer = p.buffer[startIdx+len(StartTag):] |
| 61 | continue |
| 62 | } |
| 63 | // 没找到 <tool_call>;但 buffer 末尾可能是 "<tool_call>" / "<tool_c" 等 |
| 64 | // "半个标签",需要保留若干字符避免误切。 |
| 65 | flushIndex := len(p.buffer) |
| 66 | for i := 1; i < len(StartTag); i++ { |
| 67 | if strings.HasSuffix(p.buffer, StartTag[:i]) { |
| 68 | flushIndex = len(p.buffer) - i |
| 69 | break |
| 70 | } |
| 71 | } |
| 72 | pre := p.buffer[:flushIndex] |
| 73 | if pre != "" { |
| 74 | head := strings.TrimSpace(pre) |
| 75 | // 防御:在还没有任何文本/工具被吐出的情况下,若 pre 看起来像 JSON |
| 76 | // (被截断的 tool_call),先不要输出,等下一段判断 |
| 77 | if p.emittedCount == 0 && !p.emittedText && (strings.HasPrefix(head, "{") || strings.HasPrefix(head, "`")) { |
| 78 | break |
| 79 | } |
| 80 | text.WriteString(pre) |
| 81 | if strings.TrimSpace(pre) != "" { |
| 82 | p.emittedText = true |
| 83 | } |
| 84 | } |
| 85 | p.buffer = p.buffer[flushIndex:] |
| 86 | break |
| 87 | } |
| 88 | // inside 模式:在 buffer 中寻找 </tool_call> |
| 89 | endIdx := strings.Index(p.buffer, EndTag) |
| 90 | if endIdx < 0 { |
| 91 | break |
| 92 | } |
| 93 | raw := strings.TrimSpace(p.buffer[:endIdx]) |
| 94 | if tc := buildToolCallFromRaw(raw); tc != nil { |
| 95 | toolCalls = append(toolCalls, *tc) |
| 96 | p.emittedCount++ |
| 97 | } |
| 98 | p.inside = false |