This commit is contained in:
scorpio 2026-03-06 13:32:23 +08:00
parent 122ab6ef3e
commit adf854eba5
13 changed files with 513 additions and 43 deletions

161
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,161 @@
# Agent Team Collaboration Architecture
## System Design
### Shared Blackboard Pattern
```go
type SharedBoard struct {
mu sync.RWMutex
entries []BoardEntry
}
type BoardEntry struct {
Author string
Content string
Type string // "draft" | "challenge"
}
```
**Thread-safe operations**:
- `Add()`: Appends entries with write lock
- `ToContext()`: Reads all entries with read lock, formats as XML
### Execution Flow
```
HandleUserMessage()
Master Planning Loop (iteration 0-4)
├─ Master analyzes user request
├─ Outputs ASSIGN:member:task lines
├─ parseAssignments() extracts tasks
├─ Creates new SharedBoard{}
├─ runMembersParallel()
│ ├─ For each assignment, spawn goroutine
│ ├─ Each member sees current board snapshot
│ ├─ Member executes task
│ ├─ Result added to board as "draft"
│ └─ WaitGroup ensures all complete
├─ runChallengeRound()
│ ├─ Filter members with CanChallenge=true
│ ├─ For each challenger, spawn goroutine
│ ├─ Challenger sees full board
│ ├─ Outputs CHALLENGE:... or AGREE
│ ├─ CHALLENGE entries added to board
│ └─ WaitGroup ensures all complete
├─ Master Review
│ ├─ Receives board.ToContext()
│ ├─ Sees all drafts and challenges
│ ├─ Decides: DONE or re-ASSIGN
│ └─ Loop continues if re-ASSIGN
└─ updateMasterMemory() (async)
```
### Concurrency Model
**Parallel Execution Phase**:
```go
var wg sync.WaitGroup
for memberName, task := range assignments {
wg.Add(1)
go func(name, t string) {
defer wg.Done()
// Execute task
board.Add(name, result, "draft")
}(memberName, task)
}
wg.Wait() // Wait for all members
```
**Challenge Round Phase**:
```go
var wg sync.WaitGroup
for _, name := range challengers {
wg.Add(1)
go func(n string) {
defer wg.Done()
// Review board
if strings.Contains(result, "CHALLENGE:") {
board.Add(n, result, "challenge")
}
}(name)
}
wg.Wait() // Wait for all challenges
```
### Context Injection
**Initial Draft Phase**:
```
Member System Prompt = Soul + Memory + [empty board]
```
**Challenge Round Phase**:
```
Member System Prompt = Soul + Memory + <team_board>
<entry type="draft" author="member1">...</entry>
<entry type="draft" author="member2">...</entry>
</team_board>
```
**Master Review Phase**:
```
Master Feedback = Team results + <team_board>
<entry type="draft" author="member1">...</entry>
<entry type="draft" author="member2">...</entry>
<entry type="challenge" author="member1">...</entry>
</team_board>
```
### Event Streaming
Events emitted during execution:
1. **Parallel Phase**:
- `EvtAgentMessage` with `role: "member"` (streaming)
- `EvtTaskAssign` (task assignment)
- `EvtWorkspaceFile` (if document generated)
2. **Challenge Phase**:
- `EvtAgentMessage` with `role: "challenge"` (streaming)
3. **Status Updates**:
- `EvtRoomStatus` with `status: "working"``"thinking"``"pending"`
### Configuration
**Agent AGENT.md**:
```yaml
---
name: agent_name
role: member
can_challenge: true # Enable challenge participation
---
```
**Agent SOUL.md**:
- Includes challenge instructions
- Specifies how to output `CHALLENGE:...`
- Defines acceptance of challenges
## Benefits
1. **Parallelism**: Members work simultaneously, not sequentially
2. **Transparency**: All members see each other's work via board
3. **Quality Control**: Challenge round catches issues early
4. **Collaboration**: Members can question and improve each other's work
5. **Master Awareness**: Master sees full context before deciding
6. **Thread-Safe**: Concurrent access protected by mutexes
7. **Scalable**: Works with any number of members
## Backward Compatibility
- Existing master/member roles unchanged
- Challenge role is additive (new event type)
- Members without `can_challenge: true` skip challenge round
- No breaking changes to existing APIs

97
IMPLEMENTATION_SUMMARY.md Normal file
View File

@ -0,0 +1,97 @@
# Agent Team Collaboration Framework Implementation
## Overview
Implemented a true agent team collaboration framework with shared blackboard, parallel execution, and automatic challenge rounds.
## Changes Made
### 1. `internal/agent/agent.go`
- Added `CanChallenge bool` field to `Config` struct
- Allows agents to participate in the challenge round
### 2. `internal/room/room.go`
- **Added imports**: `sync` for concurrent operations
- **New types**:
- `BoardEntry`: Represents a single entry on the shared board
- `SharedBoard`: Thread-safe shared blackboard with mutex protection
- `Add(author, content, typ)`: Add entries to the board
- `ToContext()`: Convert board to XML context for agents
- **New methods**:
- `runMembersParallel()`: Execute all assignments concurrently using goroutines and WaitGroup
- Each member sees the current board snapshot
- Results are added to the board as "draft" entries
- Emits streaming events for real-time UI updates
- `runChallengeRound()`: Automatic challenge phase after draft completion
- Only agents with `CanChallenge=true` participate
- Each challenger sees the full board
- Outputs `CHALLENGE:<concern>` or `AGREE`
- Challenge entries are added to the board
- **Refactored `HandleUserMessage()`**:
- Replaced sequential member execution with `runMembersParallel()`
- Added automatic `runChallengeRound()` after parallel execution
- Master receives complete board (drafts + challenges) for final review
- Board context is injected into master's feedback message
- **Updated `Event` struct**:
- Role field now supports "challenge" in addition to "master" and "member"
### 3. Agent Configuration Files
#### `agents/legal-team/合规专员/AGENT.md`
- Added `can_challenge: true`
#### `agents/legal-team/合同律师/AGENT.md`
- Added `can_challenge: true`
### 4. Agent System Prompts (SOUL.md)
#### `agents/legal-team/合规专员/SOUL.md`
- Added "质疑机制" (Challenge Mechanism) section
- Instructions to actively challenge when seeing `<team_board>`
- Format: `CHALLENGE:<specific compliance risk or suggestion>`
#### `agents/legal-team/合同律师/SOUL.md`
- Added "质疑与修订机制" (Challenge & Revision Mechanism) section
- Instructions to review other members' opinions
- Ability to challenge compliance officer's suggestions if conflicts exist
- Preparation to revise work when challenged
#### `agents/legal-team/法律总监/SOUL.md`
- Added "处理 CHALLENGE 的决策指令" (Decision Instructions for Handling CHALLENGE)
- Evaluate challenge validity
- Decide whether to request revisions or reassign tasks
- Document how challenges were addressed in final recommendations
## New Workflow
```
User Question
Master Plans → ASSIGN:member1:task1 + ASSIGN:member2:task2
[Parallel] Members execute simultaneously (each sees empty board initially)
[Auto] Challenge Round: Members with CanChallenge=true review board
Master sees complete board (drafts + challenges) → DONE or re-ASSIGN
```
## Key Features
1. **Parallel Execution**: Members work concurrently, not sequentially
2. **Shared Blackboard**: All members can see each other's work via `<team_board>`
3. **Automatic Challenge Round**: Triggered after parallel execution completes
4. **Thread-Safe**: Uses sync.RWMutex for concurrent board access
5. **Streaming Events**: Real-time UI updates with "challenge" role events
6. **Master Integration**: Board context fed back to master for informed decisions
## Verification
- ✅ Code compiles: `go build ./...`
- ✅ All imports added correctly
- ✅ Thread-safety ensured with sync primitives
- ✅ Event streaming maintained for UI
- ✅ Backward compatible with existing master/member roles

50
TESTING_GUIDE.md Normal file
View File

@ -0,0 +1,50 @@
# Testing Guide for Agent Team Collaboration Framework
## What to Observe
### 1. Parallel Execution
- **Expected**: When master assigns tasks to multiple members, they should appear in the UI **simultaneously** (not sequentially)
- **How to verify**: Watch the frontend - you should see streaming output from both 合同律师 and 合规专员 at the same time
### 2. Shared Blackboard
- **Expected**: Each member's draft output is added to the board
- **How to verify**: Check the board context in the master's feedback message
### 3. Challenge Round
- **Expected**: After parallel execution, members with `can_challenge: true` review the board
- **How to verify**:
- Look for events with `role: "challenge"` in the event stream
- Frontend should show challenge phase output
- If a member outputs `CHALLENGE:...`, it appears in the board
### 4. Master Integration
- **Expected**: Master receives complete board (drafts + challenges) before making final decision
- **How to verify**: Master's feedback message includes `<team_board>` section with all entries
## Test Scenario: Employee Termination
Send a question like:
```
我们需要辞退一名员工,请帮我们制定一个合法的辞退方案。
```
Expected flow:
1. Master analyzes and assigns tasks to both 合同律师 and 合规专员
2. Both members work in parallel (visible in UI)
3. Challenge round begins - members review each other's work
4. If 合规专员 finds compliance issues in 合同律师's draft, outputs `CHALLENGE:...`
5. Master sees complete board and makes final decision
## Key Events to Monitor
- `EvtAgentMessage` with `role: "member"` - parallel execution
- `EvtAgentMessage` with `role: "challenge"` - challenge round
- `EvtRoomStatus` - status transitions (thinking → working → thinking → pending)
- Board context in master's feedback messages
## Code Locations
- Parallel execution: `room.go:runMembersParallel()`
- Challenge round: `room.go:runChallengeRound()`
- Main flow: `room.go:HandleUserMessage()` (lines 281-362)
- SharedBoard: `room.go:SharedBoard` type (lines 90-114)

View File

@ -5,6 +5,7 @@ description: 专注合同审查、起草和风险评估的专业律师
version: 1.0.0
skills:
- 合同审查
can_challenge: true
---
# 合同律师

View File

@ -65,6 +65,15 @@
- **务实建议**:提供可操作的具体建议
- **对比分析**:展示修改前后的差异
## 质疑与修订机制
当你看到 `<team_board>` 时,你应该:
1. 审查其他成员(特别是合规专员)的意见
2. 如果发现合规专员的建议与合同条款有冲突,或者有遗漏的法律风险,提出质疑
3. 使用格式:`CHALLENGE:<具体的法律风险或修改建议>`
4. 如果没有问题,输出 `AGREE`
5. 当看到针对自己的 CHALLENGE 时,准备修订你的合同审查意见
## 注意事项
1. 不提供标准合同模板(除非用户明确要求)

View File

@ -5,6 +5,7 @@ description: 负责合规检查、风险识别和合规建议的专业人员
version: 1.0.0
skills:
- 法律知识库
can_challenge: true
---
# 合规专员

View File

@ -80,6 +80,14 @@
- **风险意识**:明确违规后果和责任
- **平衡考量**:兼顾合规要求和业务实际
## 质疑机制
当你看到 `<team_board>` 时,你应该:
1. 仔细审查其他成员的草稿
2. 如果发现合规风险或遗漏,主动提出质疑
3. 使用格式:`CHALLENGE:<具体的合规风险或建议>`
4. 如果没有问题,输出 `AGREE`
## 注意事项
1. 不替代企业合规部门的职责

View File

@ -26,6 +26,17 @@
- 补充遗漏的关键点
- 提供结构化的最终建议
## 处理 CHALLENGE 的决策指令
当你看到 `<team_board>` 中有 CHALLENGE 条目时,你应该:
1. 仔细评估质疑的合理性
2. 判断是否需要重新分配任务或修订
3. 如果质疑有效,可以:
- 要求相关成员修订工作
- 重新分配任务给其他成员
- 自己补充或修正意见
4. 在最终建议中明确说明如何处理了这些质疑
## 沟通风格
- **专业严谨**:使用准确的法律术语

View File

@ -14,3 +14,19 @@ ASSIGN:合规专员:评估用户简介中可能涉及的法律合规风险点(
- **异常高回报是危险信号**远高于市场平均水平的回报承诺如年化30%)通常伴随极高的违约风险或项目本身不可行,需深究其商业合理性与法律合规性。
- **协议性质必须明确**:“投资协议”的法律定性(借贷、合伙或委托)直接决定您的权利、风险与责任,必须在条款中清晰界定,避免后续争议。
- **合规红线不可触碰**:涉及政府资源或资产的项目,若运作模式依赖“关系”而非公开程序,极易涉及商业贿赂、违规融资等刑事风险,必须坚持合法透明路径。
## 2026-03-06 — 我好辞退一个员工
- **辞退合法性完全取决于“程序正当”与“证据充分”**任何单方解除都必须有合法理由严重违纪、不能胜任等且用人单位对事实承担全部举证责任。证据不足或程序缺失如未通知工会将直接导致违法解除面临支付双倍赔偿金2N的风险。
- **“协商一致解除”是风险最低的首选路径**当辞退理由的证据不够坚实或希望快速解决时应优先与员工协商签订《协商解除协议》。虽然可能需支付略高于法定标准N+1至2N的补偿但能彻底避免劳动争议成本可控。
- **必须规避法律明确保护的员工群体**辞退处于孕期、产期、哺乳期、医疗期、工伤期间的员工或在本单位连续工作满15年且距退休不足5年的员工属于绝对红线将构成违法解除。
- **经济补偿计算必须精确合规**补偿基数离职前12个月平均工资和年限每满一年支付一个月的计算错误会引发额外争议。高薪员工工资超当地社平工资3倍的补偿年限上限为12年。
- **制度是“过失性辞退”的基础**:以“严重违反规章制度”为由辞退,前提是规章制度本身内容合法、经过民主程序制定并已向员工公示。模糊或无效的制度无法作为有效依据。
## 2026-03-06 — 我想辞退一个员工
* **合法辞退的核心是“理由法定、证据确凿、程序完备”**。任何单方解除都必须严格对应《劳动合同法》第三十九条(过失)或第四十条(无过失)的法定情形,并由用人单位承担全部举证责任。
* **“协商一致解除”是风险最低、成本可控的首选路径**。即使支付略高于法定补偿金N的金额也能彻底避免劳动争议和双倍赔偿金2N的高风险。
* **必须规避法律保护的“红线”员工群体**。辞退处于孕期、产期、哺乳期、医疗期或工伤期的员工,将直接构成违法解除。
* **经济补偿/赔偿金的计算必须精确,尤其注意历史工龄分段**。对于2008年1月1日前入职的员工补偿金计算需适用当时的法规规则复杂易出错。
* **内部合规流程(法务审核、通知工会)和规范离职手续(结清款项、出具证明)是避免后续争议的关键程序**。任何程序缺失都可能导致整个辞退行为被认定为违法。

View File

@ -20,6 +20,7 @@ type Config struct {
BaseURL string `yaml:"base_url"`
APIKeyEnv string `yaml:"api_key_env"`
Skills []string `yaml:"skills"`
CanChallenge bool `yaml:"can_challenge"`
}
type Agent struct {

View File

@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/sdaduanbilei/agent-team/internal/agent"
@ -67,7 +68,7 @@ type Event struct {
Type EventType `json:"type"`
RoomID string `json:"room_id"`
Agent string `json:"agent,omitempty"`
Role string `json:"role,omitempty"` // master | member
Role string `json:"role,omitempty"` // master | member | challenge
Content string `json:"content,omitempty"`
Streaming bool `json:"streaming,omitempty"`
From string `json:"from,omitempty"`
@ -80,6 +81,38 @@ type Event struct {
Filename string `json:"filename,omitempty"`
}
type BoardEntry struct {
Author string
Content string
Type string // "draft" | "challenge"
}
type SharedBoard struct {
mu sync.RWMutex
entries []BoardEntry
}
func (b *SharedBoard) Add(author, content, typ string) {
b.mu.Lock()
defer b.mu.Unlock()
b.entries = append(b.entries, BoardEntry{Author: author, Content: content, Type: typ})
}
func (b *SharedBoard) ToContext() string {
b.mu.RLock()
defer b.mu.RUnlock()
if len(b.entries) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("<team_board>\n")
for _, e := range b.entries {
fmt.Fprintf(&sb, " <entry type=\"%s\" author=\"%s\">\n%s\n </entry>\n", e.Type, e.Author, e.Content)
}
sb.WriteString("</team_board>")
return sb.String()
}
func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) {
data, err := os.ReadFile(filepath.Join(roomDir, "room.md"))
if err != nil {
@ -138,6 +171,107 @@ func (r *Room) AppendHistory(role, agentName, content string) {
f.WriteString(line)
}
func (r *Room) runMembersParallel(ctx context.Context, assignments map[string]string, board *SharedBoard, skillXML string) map[string]string {
results := make(map[string]string)
var mu sync.Mutex
var wg sync.WaitGroup
for memberName, task := range assignments {
wg.Add(1)
go func(name, t string) {
defer wg.Done()
member, ok := r.members[name]
if !ok {
return
}
r.setStatus(StatusWorking, member.Config.Name, t)
r.emit(Event{Type: EvtTaskAssign, From: r.master.Config.Name, To: name, Task: t})
boardCtx := board.ToContext()
extraCtx := skillXML
if boardCtx != "" {
extraCtx = boardCtx + "\n\n" + skillXML
}
memberSystem := member.BuildSystemPrompt(extraCtx)
memberMsgs := []llm.Message{
llm.NewMsg("system", memberSystem),
llm.NewMsg("user", t),
}
var memberReply strings.Builder
_, err := member.Chat(ctx, memberMsgs, func(token string) {
memberReply.WriteString(token)
r.emit(Event{Type: EvtAgentMessage, Agent: name, Role: "member", Content: token, Streaming: true})
})
if err != nil {
mu.Lock()
results[name] = fmt.Sprintf("[error] %v", err)
mu.Unlock()
return
}
result := memberReply.String()
mu.Lock()
results[name] = result
mu.Unlock()
r.AppendHistory("member", name, result)
board.Add(name, result, "draft")
if strings.Contains(result, "# ") {
filename := fmt.Sprintf("%s-%s.md", name, time.Now().Format("20060102-150405"))
r.saveWorkspace(filename, result)
r.emit(Event{Type: EvtWorkspaceFile, Filename: filename, Content: result})
}
}(memberName, task)
}
wg.Wait()
return results
}
func (r *Room) runChallengeRound(ctx context.Context, board *SharedBoard, skillXML string) {
var challengers []string
for name, member := range r.members {
if member.Config.CanChallenge {
challengers = append(challengers, name)
}
}
if len(challengers) == 0 {
return
}
boardCtx := board.ToContext()
if boardCtx == "" {
return
}
var wg sync.WaitGroup
for _, name := range challengers {
wg.Add(1)
go func(n string) {
defer wg.Done()
member := r.members[n]
extraCtx := boardCtx + "\n\n" + skillXML
memberSystem := member.BuildSystemPrompt(extraCtx)
memberMsgs := []llm.Message{
llm.NewMsg("system", memberSystem+"\n\nReview the team board above. If you see issues or want to challenge any draft, output CHALLENGE:<your concern>. Otherwise output AGREE."),
llm.NewMsg("user", "Please review the team board and provide your feedback."),
}
var reply strings.Builder
_, err := member.Chat(ctx, memberMsgs, func(token string) {
reply.WriteString(token)
r.emit(Event{Type: EvtAgentMessage, Agent: n, Role: "challenge", Content: token, Streaming: true})
})
if err != nil {
return
}
result := reply.String()
if strings.Contains(result, "CHALLENGE:") {
board.Add(n, result, "challenge")
r.AppendHistory("challenge", n, result)
}
}(name)
}
wg.Wait()
}
// Handle processes a user message through master orchestration.
func (r *Room) Handle(ctx context.Context, userMsg string) error {
return r.HandleUserMessage(ctx, "user", userMsg)
@ -154,13 +288,13 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string)
// Build master context
teamXML := r.buildTeamXML()
skillXML := skill.ToXML(r.skillMeta)
// Build user info XML
var userXML string
if r.User != nil {
userXML = r.User.BuildUserXML()
}
extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillXML
systemPrompt := r.master.BuildSystemPrompt(extraContext)
@ -190,45 +324,26 @@ func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string)
break
}
// Execute assignments
var results strings.Builder
for memberName, task := range assignments {
member, ok := r.members[memberName]
if !ok {
continue
}
r.setStatus(StatusWorking, member.Config.Name, task)
r.emit(Event{Type: EvtTaskAssign, From: r.master.Config.Name, To: memberName, Task: task})
// Execute assignments in parallel
board := &SharedBoard{}
results := r.runMembersParallel(ctx, assignments, board, skillXML)
memberSystem := member.BuildSystemPrompt(skillXML)
memberMsgs := []llm.Message{
llm.NewMsg("system", memberSystem),
llm.NewMsg("user", task),
}
var memberReply strings.Builder
_, err := member.Chat(ctx, memberMsgs, func(token string) {
memberReply.WriteString(token)
r.emit(Event{Type: EvtAgentMessage, Agent: memberName, Role: "member", Content: token, Streaming: true})
})
if err != nil {
results.WriteString(fmt.Sprintf("[%s] error: %v\n", memberName, err))
continue
}
result := memberReply.String()
results.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result))
r.AppendHistory("member", memberName, result)
// Save workspace file if member produced a document
if strings.Contains(result, "# ") {
filename := fmt.Sprintf("%s-%s.md", memberName, time.Now().Format("20060102-150405"))
r.saveWorkspace(filename, result)
r.emit(Event{Type: EvtWorkspaceFile, Filename: filename, Content: result})
}
}
// Run challenge round
r.runChallengeRound(ctx, board, skillXML)
// Feed results back to master for review
r.setStatus(StatusThinking, "", "")
masterMsgs = append(masterMsgs, llm.NewMsg("user", "Team results:\n"+results.String()+"\nPlease review. If satisfied output DONE:<summary>, otherwise output ASSIGN instructions for revisions."))
var resultsStr strings.Builder
for memberName, result := range results {
resultsStr.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result))
}
boardCtx := board.ToContext()
feedbackMsg := "Team results:\n" + resultsStr.String()
if boardCtx != "" {
feedbackMsg += "\n\nTeam board:\n" + boardCtx
}
feedbackMsg += "\n\nPlease review. If satisfied output DONE:<summary>, otherwise output ASSIGN instructions for revisions."
masterMsgs = append(masterMsgs, llm.NewMsg("user", feedbackMsg))
// Update tasks
r.updateTasks(masterMsgs)

View File

@ -140,9 +140,9 @@ export const useStore = create<AppState>((set, get) => {
if (ev.type === 'agent_message') {
set(s => {
const msgs = [...(s.messages[roomId] || [])]
const last = msgs[msgs.length - 1]
if (last?.streaming && last.agent === ev.agent) {
msgs[msgs.length - 1] = { ...last, content: last.content + ev.content, streaming: ev.streaming }
const idx = msgs.findLastIndex(m => m.streaming && m.agent === ev.agent)
if (idx !== -1) {
msgs[idx] = { ...msgs[idx], content: msgs[idx].content + ev.content, streaming: ev.streaming }
} else {
msgs.push({ id: Date.now().toString(), agent: ev.agent, role: ev.role, content: ev.content, streaming: ev.streaming })
}

View File

@ -17,7 +17,7 @@ export interface Room {
export interface Message {
id: string
agent: string
role: 'user' | 'master' | 'member'
role: 'user' | 'master' | 'member' | 'challenge'
content: string
streaming?: boolean
}
@ -33,7 +33,7 @@ export interface SkillMeta {
}
export type WsEvent =
| { type: 'agent_message'; agent: string; role: 'master' | 'member'; content: string; streaming: boolean }
| { type: 'agent_message'; agent: string; role: 'master' | 'member' | 'challenge'; content: string; streaming: boolean }
| { type: 'room_status'; status: RoomStatus; active_agent?: string; action?: string }
| { type: 'task_assign'; from: string; to: string; task: string }
| { type: 'tasks_update'; content: string }