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:
| Task | Synapse | Why |
|---|---|---|
| Yes/no question | Binary | Type-safe boolean |
| Pick from options | Classification | Constrained output |
| Free-form text | Transform | String output |
| Structured data | Extract[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
| Area | Recommendation |
|---|---|
| Synapses | One task per synapse, validate custom types |
| Sessions | One per conversation, manage size, don't share |
| Reliability | Always timeout, retry with backoff, have fallback |
| Performance | Reuse synapses, match model to task |
| Observability | Track tokens, log failures, monitor latency |
| Security | Validate inputs, protect keys, don't log PII |
| Testing | Mock in unit tests, skip integration without keys |