“If you want to build AI products, you need to use Python.”
I hear this a lot, and it’s wrong. I get why people think it- software engineers have been trained to lean into open-source tooling, and the AI ecosystem is undoubtably strongest in Python.
But if you don’t already use Python, switching languages for the ecosystem is likely a bad call. Not only is that ecosystem less mature than it seems (who genuinely has enough experience to build an opinionated agent-framework at this point?), but it is grounded more in a research/ML perspective than a product one.
Additionally, the language itself is a poor fit for AI products! Python concurrency can be tricky, which is going to hurt you when it comes to optimising the latency of your AI systems, and a static type-system can bring some much-needed structure to non-deterministic models.
At incident.io, we use Go for all our backend, and saw no reason why AI should be any different. Go has been a fantastic choice, helping us build AI systems that spawn concurrent prompts, speculatively execute tools, and instrument everything to the hilt.
Despite Go being known as a no-frills language, we’ve built some really ergonomic abstractions that make working with AI really easy. I expect you can do it in your native language, too.
PromptYarr
Here’s an example of a prompt in our codebase:
type PromptYarr struct{}
type PromptYarrInput struct {
Message string `json:"text" description:"Text to translate into pirate"`
}
type PromptYarrResult struct {
Message string `json:"message" description:"Resulting pirate-speak"`
}
func (p PromptYarr) Model() string {
return goopenai.GPT4o
}
func (p PromptYarr) Result() PromptYarrResult {
return PromptYarrResult{}
}
func (p PromptYarr) Prompt(input PromptYarrInput) []goopenai.ChatCompletionMessage {
return []goopenai.ChatCompletionMessage{
{
Role: "system",
Content: `
# Context
You are a pirate translator who helps translate normal text into pirate speak.
# Task
Translate the given text into pirate speak, making sure to:
* Use common pirate phrases and terminology
* Keep the core meaning of the message intact
* Be consistent with pirate dialect throughout
* Add nautical references where appropriate
`,
},
{
Role: "user",
Content: input.Message,
},
}
}
Each prompt is its own struct, describing which model to use, the input type that we use to template the messages, and finally the prompt that we’ll send to OpenAI (or Anthropic, GCP, whatever).
What I love about this is how much heavy lifting the type system is doing for us. Running the prompt is as simple as:
result, _ := ai.RunPrompt(ctx, db, identity, PromptYarr{
Input: "Hello, how are you?",
})
// "Ahoy, how be ye?"
fmt.Println(result.Message)
Behind the scenes, RunPrompt
is doing some cool stuff:
- It reflects over our
Result
type to build an OpenAPI specification, using struct tags likedescription
andenum
as documentation. - That specification is used to force the model into giving structured JSON responses that match our type exactly.
- The prompt is templated with our
input.Message
. - Finally, the response is parsed back into our result type.
Each prompt lives in its own file (prompt_yarr.go
) alongside its evaluation
suite (prompt_yarr.yaml
), making it easy to maintain and test.
Fun with generics
The pattern above works great for fixed prompts, but sometimes you need more flexibility. That’s where Go’s generics come in handy.
Here’s an example from our incident investigation system:
gradeResult, err := ai.RunPrompt(ctx, db, identity,
ai.PromptInvestigationGrade[struct {
FoundPermalinks []string `json:"found_permalinks" description:"The permalinks of the identified code changes"`
}]{
Check: codeChangesCheck,
Artifacts: codeChangesCheck.Artifacts,
Task: `
Find the shortlist of code changes that have been found in the check as high
confidence results that relate to the incident.
`,
},
)
We’re using PromptInvestigationGrade
across our investigation
system, but each time we run it we want different types of
results back. Sometimes we want permalinks to code changes, other times we’re
extracting timeline entries, or grading the quality of our investigation
hypothesis.
Rather than copy-and-pasting the prompt in order to use the same context setup, we can use a generic type parameter to control the result we get back. The prompt template and core logic stays the same, but we get different structured data back each time.
That’s as close as we’ll get to Vercel’s ```ai
in Go, and I’d say
it’s a strong challenger.
Python is not the only answer
Python is a great tool for machine learning and a lot more. But it has real weaknesses when it comes to building production AI systems, especially if you’re already working in a different language.
I’ve spoken with several teams who switched to Python to work with AI, away from their normal stack, and they’ve all struggled. They’re trying to learn AI alongside new tools and patterns (almost always a bad idea), which makes it much more likely they burn out or fail.
Even if they succeed, they’ve disconnected their AI features from their core product. These are companies aiming to be “AI native” but have their AI features living in a separate codebase with different conventions. That doesn’t feel like success to me.
At incident.io, we kept everything in Go and it’s working really well. Our AI features live right alongside our core product, using the same development patterns the team already knows. We’ve even managed building some expressive and fancy abstractions in a language that is notoriously no-frills, while leveraging Go’s strengths in concurrency and type safety.
The AI ecosystem will continue to be Python-first, and that’s fine! The actual interaction with models is just HTTP requests with JSON payloads, though. Build good abstractions in your language of choice, and focus your energy on the hard stuff: making AI features that actually work.
If you liked this post and want to see more, follow me at @lawrjones.