Theory ultimately needs to land in production practice. This chapter uses a complete intelligent customer service system project to tie together all the knowledge from the previous nine modules and show the full engineering flow from requirements analysis, architecture design, and code implementation to production deployment. The design of this project is based on real enterprise customer-service system transformation experience; the architectural decisions, technology choices, and pitfall avoidance have all been validated in production.
Project Background and Requirements Analysis
Business Scenario
An e-commerce platform processes 500,000+ orders per day, and its customer service team handles about 80,000 user inquiries daily. Traditional human customer service faces the following pain points:
- Response latency: During peak hours, users wait an average of more than 5 minutes, leading to rising complaint rates.
- Scattered knowledge: The customer-service knowledge base is spread across five different systems, making manual retrieval inefficient.
- Multi-language support: The platform covers Southeast Asian markets and needs to support Chinese, English, Thai, and Vietnamese.
- Complex business: Inquiries involve five major business domains—orders, logistics, after-sales, promotions, and payments—each with independent internal systems.
- Peak pressure: During big promotions, inquiry volume can reach ten times the usual amount, and manual scaling is costly.
Functional Requirements
Based on the above pain points, the intelligent customer service system must satisfy:
| Requirement | Description | Priority |
|---|---|---|
| Automatic Q&A | After a user asks a question, the Agent understands the intent and answers automatically | P0 |
| Multi-turn conversation | Supports context memory and can ask follow-up questions and clarify | P0 |
| Multi-domain routing | Routes questions to the corresponding business Agent based on type | P0 |
| Internal system integration | Real-time query of orders, logistics, inventory, and other internal data | P0 |
| Streaming output | Answers appear in a typewriter effect in real time to improve user experience | P1 |
| Human handoff | Seamlessly transfer to a human agent when confidence is below a threshold | P1 |
| Multi-language support | Automatically detect the user’s language and reply in the same language | P1 |
| Data analytics | Collect conversation data for continuous optimization and reporting | P2 |
Non-Functional Requirements
| Metric | Target | Description |
|---|---|---|
| Availability | 99.9% | Annual downtime < 8.76 hours |
| P95 response latency | < 3 s | From user sending a message to first token returned |
| P99 response latency | < 8 s | Tolerate a small amount of long-tail latency |
| Concurrent throughput | 5,000 QPS | Peak capacity during big promotions |
| Answer accuracy | > 85% | Based on manual sampling evaluation |
| Data security | Level 3 protection | User conversation data encrypted at rest |
System Architecture Design
Overall Architecture
The system uses a layered + microservices architecture. Core components include:
┌─────────────────────────────────────────────────────────────┐
│ Access Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web Chat │ │ Mini-App CS │ │ API Gateway │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼─────────────────┼─────────────────┼──────────────┘
│ │ │
└─────────────────┼─────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Gateway & Routing Layer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Kong / Nginx (rate limiting, auth) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Core Service Layer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Conversation Orchestration Service │ │
│ │ (Go + ADK) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Router │ │ Memory │ │ Session State │ │ │
│ │ │ Agent │ │ Manager │ │ Machine │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Order Agent │ │ Logistics Agent│ │ After-Sales │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ │
│ │ Order Service│ │ Logistics Svc│ │ After-Sales │ │
│ │ API │ │ API │ │ API │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │ │
└─────────────────┼─────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Redis │ │PostgreSQL│ │ Kafka │ │Prometheus│ │
│ │(session │ │(persist) │ │(events) │ │(monitor) │ │
│ │ cache) │ │ │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
Key Architecture Decisions
1. Why Router + Sub-Agent instead of a single large Agent?
Problems with a single large Agent:
- The context window is squeezed by knowledge from different domains, reducing depth in each domain.
- When the number of Tools grows too large, the model’s Tool-selection accuracy drops significantly (we found accuracy fell from 92% to 78% when exceeding 15 Tools).
- It is impossible to optimize prompts and model parameters for each business domain.
- Single point of failure: a Tool failure in one domain can affect the entire system.
Router + Sub-Agent advantages:
- Each Sub-Agent focuses on one domain with more precise prompts.
- Can be deployed and scaled independently.
- Fault isolation: a logistics Agent failure does not affect order queries.
2. Why Go + ADK instead of the Python ecosystem?
- Performance: Go’s concurrency model (goroutines) is more resource-efficient in high-concurrency scenarios.
- Deployment: Go compiles to a single binary, container images are small (usually < 50 MB), and startup is fast (< 100 ms).
- Type safety: The static type system significantly reduces runtime errors in large projects.
- Team skill stack: The team already had Go microservices experience; a unified stack lowers maintenance costs.
- A2A protocol: ADK’s A2A protocol supports cross-language Agent communication, so Python-ecosystem Agents can be integrated when necessary.
3. Session State Management Strategy
type SessionStore interface {
// Short-term cache: Redis, TTL 30 minutes
GetShortTerm(ctx context.Context, sessionID string) (*SessionState, error)
SetShortTerm(ctx context.Context, sessionID string, state *SessionState, ttl time.Duration) error
// Long-term persistence: PostgreSQL, used for data analytics
Persist(ctx context.Context, sessionID string, history []Message) error
}
type SessionState struct {
SessionID string
UserID string
Language string
CurrentAgent string // currently active Sub-Agent
Context map[string]interface{} // cross-turn context
CreatedAt time.Time
LastActive time.Time
}
Core Code Implementation
1. Project Initialization and Dependency Management
# Create project
go mod init github.com/yourcompany/customer-service-agent
# Core dependencies
go get google.golang.org/adk@latest
go get github.com/redis/go-redis/v9
go get github.com/lib/pq
go get github.com/prometheus/client_golang/prometheus
2. Internal API Tool Implementation (with Circuit Breaker and Cache)
package tools
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/sony/gobreaker"
)
// OrderServiceClient wraps order-service API calls
type OrderServiceClient struct {
baseURL string
httpClient *http.Client
breaker *gobreaker.CircuitBreaker
cache *Cache // local cache for hot data
}
func NewOrderServiceClient(baseURL string) *OrderServiceClient {
return &OrderServiceClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 3 * time.Second,
},
breaker: gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "order-service",
MaxRequests: 100,
Interval: 10 * time.Second,
Timeout: 5 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 10 && failureRatio >= 0.5
},
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
log.Printf("Circuit breaker state changed: %s %s -> %s", name, from, to)
},
}),
cache: NewCache(5 * time.Minute),
}
}
type OrderInfo struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
Items []OrderItem `json:"items"`
}
type OrderItem struct {
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
}
func (c *OrderServiceClient) QueryOrder(ctx context.Context, orderID string) (*OrderInfo, error) {
// 1. Check cache
if cached, ok := c.cache.Get(orderID); ok {
return cached.(*OrderInfo), nil
}
// 2. API call protected by circuit breaker
result, err := c.breaker.Execute(func() (interface{}, error) {
url := fmt.Sprintf("%s/api/v1/orders/%s", c.baseURL, orderID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+getServiceToken())
req.Header.Set("X-Request-ID", getRequestID(ctx))
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("order service request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("order service returned error: %d", resp.StatusCode)
}
var order OrderInfo
if err := json.NewDecoder(resp.Body).Decode(&order); err != nil {
return nil, fmt.Errorf("failed to decode order data: %w", err)
}
return &order, nil
})
if err != nil {
return nil, err
}
order := result.(*OrderInfo)
// 3. Write to cache (completed orders cached 1 hour, in-progress orders cached 30 seconds)
ttl := 1 * time.Hour
if order.Status == "processing" || order.Status == "shipped" {
ttl = 30 * time.Second
}
c.cache.Set(orderID, order, ttl)
return order, nil
}
// Tool interface implementation
func (c *OrderServiceClient) Name() string {
return "query_order"
}
func (c *OrderServiceClient) Description() string {
return "Query order details. Parameter: order_id (string) - order number"
}
func (c *OrderServiceClient) Call(ctx context.Context, args map[string]interface{}) (string, error) {
orderID, ok := args["order_id"].(string)
if !ok || orderID == "" {
return "", fmt.Errorf("missing required parameter: order_id")
}
// Order ID format validation
if !isValidOrderID(orderID) {
return "", fmt.Errorf("invalid order ID format: %s", orderID)
}
order, err := c.QueryOrder(ctx, orderID)
if err != nil {
// Distinguish error types to give the model different feedback
if err == gobreaker.ErrOpenState {
return "Order service is temporarily unavailable, please try again later", nil
}
return "", fmt.Errorf("query order failed: %w", err)
}
// Format as model-friendly text
return fmt.Sprintf(`Order information:
- Order ID: %s
- Status: %s
- Amount: $%.2f
- Order time: %s
- Items: %s`,
order.OrderID,
order.Status,
order.Amount,
order.CreatedAt.Format("2006-01-02 15:04"),
formatItems(order.Items),
), nil
}
3. Multi-Agent Routing Implementation
package agents
import (
"context"
"fmt"
"strings"
"google.golang.org/adk/agent"
"google.golang.org/adk/llm"
"google.golang.org/adk/team"
)
// CustomerServiceRouter implements intelligent routing
type CustomerServiceRouter struct {
router *team.Router
agents map[string]*agent.Agent
fallback *agent.Agent // fallback Agent
}
func NewCustomerServiceRouter(model llm.Model) (*CustomerServiceRouter, error) {
r := &CustomerServiceRouter{agents: make(map[string]*agent.Agent)}
// Create business-domain Agents
orderAgent, err := r.createOrderAgent(model)
if err != nil {
return nil, fmt.Errorf("failed to create order Agent: %w", err)
}
r.agents["order"] = orderAgent
logisticsAgent, err := r.createLogisticsAgent(model)
if err != nil {
return nil, fmt.Errorf("failed to create logistics Agent: %w", err)
}
r.agents["logistics"] = logisticsAgent
afterSalesAgent, err := r.createAfterSalesAgent(model)
if err != nil {
return nil, fmt.Errorf("failed to create after-sales Agent: %w", err)
}
r.agents["after_sales"] = afterSalesAgent
// Fallback Agent for general questions
fallbackAgent, err := agent.New(agent.Config{
Name: "fallback-agent",
Model: model,
Instruction: `You are a general customer-service assistant. When the user's question does not belong to order, logistics, or after-sales domains, provide friendly general help. If the question is beyond your ability, honestly tell the user and suggest contacting a human agent.`,
})
if err != nil {
return nil, err
}
r.fallback = fallbackAgent
// Configure routing rules
r.router = team.NewRouter(
team.WithClassifier(r.classifyQuery),
team.WithFallback(fallbackAgent),
team.WithRoute("order", r.agents["order"]),
team.WithRoute("logistics", r.agents["logistics"]),
team.WithRoute("after_sales", r.agents["after_sales"]),
)
return r, nil
}
// classifyQuery implements query classification logic
func (r *CustomerServiceRouter) classifyQuery(ctx context.Context, query string) (string, float64) {
query = strings.ToLower(query)
// Fast keyword-based classification (O(1) latency)
orderKeywords := []string{"order", "place order", "purchase", "payment", "cancel", "price"}
logisticsKeywords := []string{"logistics", "express", "ship", "delivery", "where", "signed", "station"}
afterSalesKeywords := []string{"return", "exchange", "repair", "warranty", "after-sales", "complaint", "compensation"}
for _, kw := range orderKeywords {
if strings.Contains(query, kw) {
return "order", 0.9
}
}
for _, kw := range logisticsKeywords {
if strings.Contains(query, kw) {
return "logistics", 0.9
}
}
for _, kw := range afterSalesKeywords {
if strings.Contains(query, kw) {
return "after_sales", 0.9
}
}
// If keywords miss, use model-based semantic classification (higher latency but more accurate)
// In production, cache classification results for common queries
return r.semanticClassify(ctx, query)
}
func (r *CustomerServiceRouter) semanticClassify(ctx context.Context, query string) (string, float64) {
// Use a lightweight classifier or let an LLM classify
// To save tokens, use a simplified few-shot prompt
prompt := fmt.Sprintf(`Classify the following user query into one of: order, logistics, after_sales, other.
Output only the label.
Examples:
Query: Where is my package?
Label: logistics
Query: %s
Label:`, query)
// Call a lightweight model for classification
// ...
return "other", 0.5
}
func (r *CustomerServiceRouter) Run(ctx context.Context, sessionID, query string) (*agent.Response, error) {
return r.router.Run(ctx, sessionID, query)
}
func (r *CustomerServiceRouter) createOrderAgent(model llm.Model) (*agent.Agent, error) {
return agent.New(agent.Config{
Name: "order-agent",
Model: model,
Instruction: `You are an e-commerce order-specialist customer-service agent. Your responsibilities are:
1. Help users query order status and details
2. Handle order modification requests (e.g., change address, cancel order)
3. Explain order-related policies and rules
4. Handle payment-related questions
Notes:
- For operations involving funds (refunds, compensation), you must transfer to a human agent
- Do not promise a specific processing time; use wording such as "usually takes X-Y business days"
- When the user is emotional, calm them first before solving the problem`,
Tools: []tool.Tool{
tools.NewOrderServiceClient(os.Getenv("ORDER_SERVICE_URL")),
tools.NewPaymentServiceClient(os.Getenv("PAYMENT_SERVICE_URL")),
},
})
}
4. Streaming Output and WebSocket Integration
package server
import (
"context"
"net/http"
"time"
"github.com/gorilla/websocket"
)
type WebSocketServer struct {
router *agents.CustomerServiceRouter
upgrader websocket.Upgrader
sessions *SessionManager
}
func (s *WebSocketServer) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
sessionID := r.URL.Query().Get("session_id")
if sessionID == "" {
sessionID = generateSessionID()
}
for {
_, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("WebSocket unexpected close: %v", err)
}
break
}
userMsg := string(message)
// Streaming processing
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
go func() {
defer cancel()
stream, err := s.router.RunStream(ctx, sessionID, userMsg)
if err != nil {
conn.WriteJSON(map[string]string{
"type": "error",
"content": "Service temporarily unavailable, please try again later",
})
return
}
for chunk := range stream {
if err := conn.WriteJSON(map[string]string{
"type": "chunk",
"content": chunk.Text,
}); err != nil {
log.Printf("Failed to send streaming message: %v", err)
return
}
}
// Stream end marker
conn.WriteJSON(map[string]string{
"type": "done",
})
}()
}
}
5. Production Deployment Configuration
Dockerfile:
# Build stage
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o customer-service-agent ./cmd/server
# Run stage
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /app/customer-service-agent .
COPY --from=builder /app/configs ./configs
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["./customer-service-agent"]
Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: customer-service-agent
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: customer-service-agent
template:
metadata:
labels:
app: customer-service-agent
spec:
containers:
- name: agent
image: your-registry/customer-service-agent:v1.2.0
ports:
- containerPort: 8080
env:
- name: GOOGLE_API_KEY
valueFrom:
secretKeyRef:
name: agent-secrets
key: google-api-key
- name: REDIS_URL
valueFrom:
configMapKeyRef:
name: agent-config
key: redis-url
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Complete Code Structure
customer-service-agent/
├── cmd/
│ └── server/
│ └── main.go # service entry
├── internal/
│ ├── agents/
│ │ ├── router.go # router Agent
│ │ ├── order_agent.go # order Agent
│ │ ├── logistics_agent.go # logistics Agent
│ │ └── after_sales_agent.go # after-sales Agent
│ ├── tools/
│ │ ├── order.go # order-service Tool
│ │ ├── logistics.go # logistics-service Tool
│ │ ├── payment.go # payment-service Tool
│ │ └── base.go # Tool base class and common logic
│ ├── server/
│ │ ├── websocket.go # WebSocket service
│ │ ├── http.go # HTTP API
│ │ └── middleware.go # middleware (auth, rate limiting, logging)
│ ├── session/
│ │ ├── manager.go # session management
│ │ ├── redis_store.go # Redis storage implementation
│ │ └── postgres_store.go # PostgreSQL persistence
│ ├── config/
│ │ └── config.go # configuration management
│ └── metrics/
│ └── metrics.go # Prometheus metrics
├── configs/
│ ├── production.yaml
│ └── staging.yaml
├── deployments/
│ ├── Dockerfile
│ ├── k8s-deployment.yaml
│ └── docker-compose.yml
├── go.mod
├── go.sum
└── README.md
Summary
The end-to-end project is complete. This project synthesizes knowledge from the following modules:
- Module 2: Basic Agent configuration and initialization
- Module 3: Tool implementation, including external API integration, circuit breaker, and caching
- Module 4: Session management and memory persistence
- Module 5: Multi-Agent collaboration and routing
- Module 6: Streaming output and WebSocket real-time communication
- Module 7: Docker containerization and Kubernetes deployment
- Module 8: A2A protocol (reserved extension point for integrating Python-ecosystem Agents)
- Module 9: Callback monitoring and Plugin observability integration
Next we move on to the pitfall notes—summarizing real-world lessons learned.
← Callbacks & Plugins | Debugging & Tuning →
Want to learn more Go ADK hands-on? Follow the “Full-Stack Peak — Mengshou Programming” WeChat account for weekly Go / AI programming practice updates.
