zoobzio December 13, 2025 Edit this page

Best Practices

Guidelines for building production LLM applications with zyn.

Synapse Design

Keep Tasks Focused

Each synapse should do one thing well:

// ✅ Good: Single, clear task
classifier, _ := zyn.Classification("email type", categories, provider)
extractor, _ := zyn.Extract[Contact]("contact info", provider)

// ❌ Bad: Multiple tasks in one
synapse, _ := zyn.Transform("classify, extract contacts, and summarize", provider)

Use Appropriate Synapse Types

Choose the right synapse for the task:

TaskSynapseWhy
Yes/no questionBinaryType-safe boolean
Pick from optionsClassificationConstrained output
Free-form textTransformString output
Structured dataExtract[T]Type-safe struct

Validate Custom Types

Always implement meaningful validation:

type Order struct {
    ID     string  `json:"id"`
    Amount float64 `json:"amount"`
}

func (o Order) Validate() error {
    if o.ID == "" {
        return fmt.Errorf("order ID required")
    }
    if o.Amount <= 0 {
        return fmt.Errorf("amount must be positive")
    }
    if o.Amount > 1000000 {
        return fmt.Errorf("amount exceeds maximum")
    }
    return nil
}

Session Management

One Session Per Conversation

// ✅ Good: Dedicated session per user/conversation
userSessions := make(map[string]*zyn.Session)

func handleRequest(userID, input string) {
    session := userSessions[userID]
    if session == nil {
        session = zyn.NewSession()
        userSessions[userID] = session
    }
    synapse.Fire(ctx, session, input)
}

Manage Session Size

Sessions grow unbounded. Implement pruning:

const maxMessages = 50

func processWithPruning(ctx context.Context, session *zyn.Session, input string) {
    result, _ := synapse.Fire(ctx, session, input)

    // Prune if too large
    if session.Len() > maxMessages {
        session.Prune(session.Len() - maxMessages)
    }

    return result
}

Don't Share Sessions Across Users

// ❌ Bad: Shared session leaks context between users
var globalSession = zyn.NewSession()

// ✅ Good: Isolated sessions
func newUserSession() *zyn.Session {
    return zyn.NewSession()
}

Reliability

Always Use Timeout

LLM calls can hang indefinitely:

// ✅ Good: Bounded execution time
synapse, _ := zyn.Binary("q", provider, zyn.WithTimeout(30*time.Second))

// ❌ Bad: Unbounded
synapse, _ := zyn.Binary("q", provider)

Retry with Backoff

For production, use backoff to handle rate limits:

synapse, _ := zyn.Binary("q", provider,
    zyn.WithBackoff(3, 100*time.Millisecond),  // 100ms, 200ms, 400ms
    zyn.WithTimeout(30*time.Second),
)

Circuit Breaker for Outages

Protect against cascading failures:

synapse, _ := zyn.Binary("q", provider,
    zyn.WithCircuitBreaker(5, 60*time.Second),
)

Have a Fallback

For critical paths:

backupSynapse, _ := zyn.Binary("q", backupProvider)
synapse, _ := zyn.Binary("q", primaryProvider,
    zyn.WithFallback(backupSynapse),
)

Performance

Reuse Synapses

Create synapses once, reuse them:

// ✅ Good: Create once
var classifier *zyn.ClassificationSynapse

func init() {
    classifier, _ = zyn.Classification("type", categories, provider)
}

func handleRequest(input string) {
    session := zyn.NewSession()
    return classifier.Fire(ctx, session, input)
}

// ❌ Bad: Create per request
func handleRequest(input string) {
    classifier, _ := zyn.Classification("type", categories, provider)
    return classifier.Fire(ctx, zyn.NewSession(), input)
}

Use Appropriate Models

Match model capability to task complexity:

// Simple classification → fast, cheap model
classifier, _ := zyn.Classification("type", cats, openai.New(openai.Config{
    Model: "gpt-4o-mini",
}))

// Complex reasoning → capable model
analyzer, _ := zyn.Analyze[Report]("deep analysis", openai.New(openai.Config{
    Model: "gpt-4o",
}))

Temperature for Determinism

Use low temperature for repeatable results. Classification already defaults to 0.3, but you can override per-request:

// Override temperature via input struct
input := zyn.ClassificationInput{
    Subject:     "some content",
    Temperature: zyn.DefaultTemperatureDeterministic, // 0.1
}
result, _ := classifier.FireWithInput(ctx, session, input)

Observability

Track Token Usage

Monitor costs:

capitan.Hook(zyn.ProviderCallCompleted, func(ctx context.Context, e *capitan.Event) {
    tokens, _ := zyn.TotalTokensKey.From(e)
    model, _ := zyn.ModelKey.From(e)
    metrics.Add("llm_tokens", float64(tokens), "model", model)
})

Log Failures

capitan.Hook(zyn.RequestFailed, func(ctx context.Context, e *capitan.Event) {
    requestID, _ := zyn.RequestIDKey.From(e)
    err, _ := zyn.ErrorKey.From(e)
    log.Printf("Request %s failed: %s", requestID, err)
})

Monitor Latency

capitan.Hook(zyn.ProviderCallCompleted, func(ctx context.Context, e *capitan.Event) {
    duration, _ := zyn.DurationMsKey.From(e)
    metrics.Histogram("llm_latency_ms", float64(duration))
})

Security

Validate All Inputs

Don't pass user input directly without validation:

func handleUserInput(input string) {
    // Validate/sanitize
    if len(input) > 10000 {
        return errors.New("input too long")
    }

    synapse.Fire(ctx, session, input)
}

Don't Log Sensitive Data

capitan.Hook(zyn.RequestCompleted, func(ctx context.Context, e *capitan.Event) {
    // ❌ Bad: May contain PII
    input, _ := zyn.InputKey.From(e)
    log.Printf("Input: %s", input)

    // ✅ Good: Log metadata only
    requestID, _ := zyn.RequestIDKey.From(e)
    log.Printf("Request %s completed", requestID)
})

Protect API Keys

// ✅ Good: Environment variable
provider := openai.New(openai.Config{
    APIKey: os.Getenv("OPENAI_API_KEY"),
})

// ❌ Bad: Hardcoded
provider := openai.New(openai.Config{
    APIKey: "sk-...",
})

Testing

Use Mocks in Unit Tests

func TestBusiness Logic(t *testing.T) {
    provider := zyn.NewMockProviderWithResponse(`{...}`)
    // Test with deterministic responses
}

Integration Tests with Skip

func TestRealProvider(t *testing.T) {
    if os.Getenv("OPENAI_API_KEY") == "" {
        t.Skip("OPENAI_API_KEY not set")
    }
    // Test against real provider
}

Summary

AreaRecommendation
SynapsesOne task per synapse, validate custom types
SessionsOne per conversation, manage size, don't share
ReliabilityAlways timeout, retry with backoff, have fallback
PerformanceReuse synapses, match model to task
ObservabilityTrack tokens, log failures, monitor latency
SecurityValidate inputs, protect keys, don't log PII
TestingMock in unit tests, skip integration without keys